From 48e933734174daad454e2b40445a8936329c060a Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Sat, 4 Nov 2017 22:07:12 -0400 Subject: [PATCH 01/43] Started work on secondary partial verification - Working partial verification for secondaries - secondary.process_metadata calls partial verification if partial_verifying=True - secondary.process_partial_metadata accepts the directors targets from the primary - Verifies it using updater._verify_uncompressed_metadata_file - Reorganizes the metadata from the targets file to match that of the metadata files used by the ECU to install without any issue --- demo/demo_primary.py | 1 - demo/demo_secondary.py | 36 ++++++++++++++++++------- uptane/clients/secondary.py | 52 ++++++++++++++++++++++++++++++------- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/demo/demo_primary.py b/demo/demo_primary.py index 1ae9158..98647a9 100644 --- a/demo/demo_primary.py +++ b/demo/demo_primary.py @@ -488,7 +488,6 @@ def get_metadata_for_ecu(ecu_serial, force_partial_verification=False): """ # Ensure serial is correct format & registered primary_ecu._check_ecu_serial(ecu_serial) - # The filename of the file to return. fname = None diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index bba2027..836e58c 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -79,7 +79,8 @@ def clean_slate( vin=_vin, ecu_serial=_ecu_serial, primary_host=None, - primary_port=None): + primary_port=None, + partial_verifying=False): """ """ @@ -88,12 +89,13 @@ def clean_slate( global _ecu_serial global _primary_host global _primary_port + global _partial_verifying global nonce global CLIENT_DIRECTORY global attacks_detected - _vin = vin _ecu_serial = ecu_serial + _partial_verifying = partial_verifying if primary_host is not None: _primary_host = primary_host @@ -142,6 +144,11 @@ def clean_slate( tuf.conf.repository_directory = CLIENT_DIRECTORY # This setting should probably be called CLIENT_DIRECTORY instead, post-TAP4. + #Importing director's public key if secondary is partial verification + if _partial_verifying: + key_director_pub = demo.import_public_key('director') + else: + key_director_pub = None # Initialize a full verification Secondary ECU. # This also generates a nonce to use in the next time query, sets the initial @@ -154,7 +161,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, + director_public_key = key_director_pub, + partial_verifying = _partial_verifying) @@ -302,8 +311,7 @@ def update_cycle(): # Download the metadata from the Primary in the form of an archive. This # returns the binary data that we need to write to file. - metadata_archive = pserver.get_metadata(secondary_ecu.ecu_serial) - + metadata_archive = pserver.get_metadata(secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) # Validate the time attestation and internalize the time. Continue # regardless. try: @@ -321,14 +329,22 @@ def update_cycle(): # print(GREEN + 'Official time has been updated successfully.' + ENDCOLORS) # Dump the archive file to disk. - archive_fname = os.path.join( - secondary_ecu.full_client_dir, 'metadata_archive.zip') - - with open(archive_fname, 'wb') as fobj: - fobj.write(metadata_archive.data) + if secondary_ecu.partial_verifying: + archive_fname = os.path.join( + secondary_ecu.full_client_dir, 'director_targets.'+tuf.conf.METADATA_FORMAT) + with open(archive_fname, 'wb') as f: + f.write(metadata_archive.data) + else: + archive_fname = os.path.join( + secondary_ecu.full_client_dir, 'metadata_archive.zip') + with open(archive_fname, 'wb') as fobj: + fobj.write(metadata_archive.data) # Now tell the Secondary reference implementation code where the archive file # is and let it expand and validate the metadata. + # if secondary_ecu.partial_verifying: + # secondary_ecu.process_partial_metadata(archive_fname) + # else: secondary_ecu.process_metadata(archive_fname) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 393436c..d9acb84 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -449,8 +449,6 @@ def validate_time_attestation(self, timeserver_attestation): - - def fully_validate_metadata(self): """ Treats the unvalidated metadata obtained from the Primary (which the @@ -499,7 +497,6 @@ def fully_validate_metadata(self): 'ecu_serial' not in target['fileinfo']['custom'] or \ self.ecu_serial != target['fileinfo']['custom']['ecu_serial']: continue - # Fully validate the target info for our target(s). try: validated_targets_for_this_ecu.append( @@ -509,8 +506,7 @@ def fully_validate_metadata(self): repr(target['filepath']) + ', which the Director assigned to this ' 'Secondary ECU, using the validation rules in pinned.json' + ENDCOLORS) - continue - + continue self.validated_targets_for_this_ecu = validated_targets_for_this_ecu @@ -556,14 +552,51 @@ def process_metadata(self, metadata_archive_fname): Fully validate the metadata for the target file(s) """ tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) + if self.partial_verifying: + self.process_partial_metadata(metadata_archive_fname) + #self._metadata(metadata_archive_fname) + else: + self._expand_metadata_archive(metadata_archive_fname) - self._expand_metadata_archive(metadata_archive_fname) + # This entails using the local metadata files as a repository. + self.fully_validate_metadata() - # This entails using the local metadata files as a repository. - self.fully_validate_metadata() + def process_partial_metadata(self, metadata_archive_fname): + """ + Implementation for partial verification in secondaries. + Accepts the archive fname. Processes it to only get a tuf.util.tempfile + for Director's targets metadata from the archive. + Checks the targets metadata for the different conditions and attacks as + specified in the implementation requirements. + If it passes all checks, the update will be installed. + """ + tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) + validated_targets_for_this_ecu = [] + target_metadata = {} + if not os.path.exists(metadata_archive_fname): + raise uptane.Error('Indicated metadata archive does not exist. ' + 'Filename: ' + repr(metadata_archive_fname)) + + #z = zipfile.ZipFile(metadata_archive_fname) + #z.extract('director/metadata/targets.der', self.full_client_dir) + #director_target_file = os.path.join(self.full_client_dir, 'director', 'metadata', 'targets.der') + metadata_file_object = tuf.util.TempFile() + with open(metadata_archive_fname, 'rb') as f: + data = f.read() + metadata_file_object.write(data) + self.updater.repositories[self.director_repo_name].\ + _verify_uncompressed_metadata_file(metadata_file_object, 'targets') + + data = tuf.util.load_file(metadata_archive_fname) + targets = data['signed']['targets'] + for file in targets.keys(): + target_metadata['filepath'] = file + target_metadata['fileinfo'] = targets[file] + validated_targets_for_this_ecu.append(target_metadata) + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu def _expand_metadata_archive(self, metadata_archive_fname): @@ -586,13 +619,14 @@ def _expand_metadata_archive(self, metadata_archive_fname): 'Filename: ' + repr(metadata_archive_fname)) z = zipfile.ZipFile(metadata_archive_fname) - z.extractall(os.path.join(self.full_client_dir, 'unverified')) + + def validate_image(self, image_fname): """ Determines if the image with filename provided matches the expected file From 4b113d99824e9522c1b0f5c9da6b6b3cacd19e34 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Thu, 28 Dec 2017 17:53:39 -0500 Subject: [PATCH 02/43] Code review changes in secondary, demo_secondary and demo_primary Removed some left-over print statements --- demo/demo_secondary.py | 34 +++++++++++++++------------ uptane/clients/secondary.py | 46 +++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index 836e58c..90c05ac 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -89,13 +89,11 @@ def clean_slate( global _ecu_serial global _primary_host global _primary_port - global _partial_verifying global nonce global CLIENT_DIRECTORY global attacks_detected _vin = vin _ecu_serial = ecu_serial - _partial_verifying = partial_verifying if primary_host is not None: _primary_host = primary_host @@ -145,7 +143,7 @@ def clean_slate( #Importing director's public key if secondary is partial verification - if _partial_verifying: + if partial_verifying: key_director_pub = demo.import_public_key('director') else: key_director_pub = None @@ -163,7 +161,7 @@ def clean_slate( firmware_fileinfo=factory_firmware_fileinfo, timeserver_public_key=key_timeserver_pub, director_public_key = key_director_pub, - partial_verifying = _partial_verifying) + partial_verifying = partial_verifying) @@ -309,9 +307,14 @@ def update_cycle(): # from it like so: time_attestation = time_attestation.data - # Download the metadata from the Primary in the form of an archive. This - # returns the binary data that we need to write to file. - metadata_archive = pserver.get_metadata(secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) + # If the secondary runs in full verification mode: + # The metadata is downloaded from the Primary in the form of an archive + # that includes all the metadata files. This returns the binary data that + # we need to write to the file. + # If the secondary runs in partial verification mode: + # Only the director's targets role file is copied. + + metadata_from_primary = pserver.get_metadata(secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) # Validate the time attestation and internalize the time. Continue # regardless. try: @@ -328,23 +331,24 @@ def update_cycle(): #else: # print(GREEN + 'Official time has been updated successfully.' + ENDCOLORS) + # If secondary is running full validation then # Dump the archive file to disk. + # If secondary is running partial verification then + # copies the director targets role file. if secondary_ecu.partial_verifying: - archive_fname = os.path.join( + director_targets_role = os.path.join( secondary_ecu.full_client_dir, 'director_targets.'+tuf.conf.METADATA_FORMAT) - with open(archive_fname, 'wb') as f: - f.write(metadata_archive.data) + with open(director_targets_role, 'wb') as f: + f.write(metadata_from_primary.data) else: archive_fname = os.path.join( - secondary_ecu.full_client_dir, 'metadata_archive.zip') + secondary_ecu.full_client_dir, 'metadata_from_primary.zip') with open(archive_fname, 'wb') as fobj: - fobj.write(metadata_archive.data) + fobj.write(metadata_from_primary.data) # Now tell the Secondary reference implementation code where the archive file # is and let it expand and validate the metadata. - # if secondary_ecu.partial_verifying: - # secondary_ecu.process_partial_metadata(archive_fname) - # else: + secondary_ecu.process_metadata(archive_fname) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index d9acb84..7f473af 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -449,6 +449,8 @@ def validate_time_attestation(self, timeserver_attestation): + + def fully_validate_metadata(self): """ Treats the unvalidated metadata obtained from the Primary (which the @@ -497,6 +499,7 @@ def fully_validate_metadata(self): 'ecu_serial' not in target['fileinfo']['custom'] or \ self.ecu_serial != target['fileinfo']['custom']['ecu_serial']: continue + # Fully validate the target info for our target(s). try: validated_targets_for_this_ecu.append( @@ -506,7 +509,8 @@ def fully_validate_metadata(self): repr(target['filepath']) + ', which the Director assigned to this ' 'Secondary ECU, using the validation rules in pinned.json' + ENDCOLORS) - continue + continue + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu @@ -554,7 +558,7 @@ def process_metadata(self, metadata_archive_fname): tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) if self.partial_verifying: self.process_partial_metadata(metadata_archive_fname) - #self._metadata(metadata_archive_fname) + else: self._expand_metadata_archive(metadata_archive_fname) @@ -564,41 +568,45 @@ def process_metadata(self, metadata_archive_fname): - def process_partial_metadata(self, metadata_archive_fname): + + def process_partial_metadata(self, director_targets_metadata): """ - Implementation for partial verification in secondaries. - Accepts the archive fname. Processes it to only get a tuf.util.tempfile - for Director's targets metadata from the archive. + Implementation for partial verification in secondaries. + Accepts the director targets metadata. + Processes it to only get a tuf.util.tempfile to hold the director + target's metadata. Checks the targets metadata for the different conditions and attacks as - specified in the implementation requirements. - If it passes all checks, the update will be installed. + specified in the implementation requirements. + Performs partial validation and stores the directors target info + recieved for a validated target (hash, length, etc.). + """ - tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) + tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) validated_targets_for_this_ecu = [] target_metadata = {} - if not os.path.exists(metadata_archive_fname): + if not os.path.exists(director_targets_metadata): raise uptane.Error('Indicated metadata archive does not exist. ' - 'Filename: ' + repr(metadata_archive_fname)) + 'Filename: ' + repr(director_targets_metadata)) - #z = zipfile.ZipFile(metadata_archive_fname) - #z.extract('director/metadata/targets.der', self.full_client_dir) - #director_target_file = os.path.join(self.full_client_dir, 'director', 'metadata', 'targets.der') metadata_file_object = tuf.util.TempFile() - with open(metadata_archive_fname, 'rb') as f: + with open(director_targets_metadata, 'rb') as f: data = f.read() metadata_file_object.write(data) self.updater.repositories[self.director_repo_name].\ _verify_uncompressed_metadata_file(metadata_file_object, 'targets') - - data = tuf.util.load_file(metadata_archive_fname) + + data = tuf.util.load_file(director_targets_metadata) targets = data['signed']['targets'] for file in targets.keys(): - target_metadata['filepath'] = file + target_metadata['filepath'] = file target_metadata['fileinfo'] = targets[file] validated_targets_for_this_ecu.append(target_metadata) self.validated_targets_for_this_ecu = validated_targets_for_this_ecu + + + def _expand_metadata_archive(self, metadata_archive_fname): """ Given the filename of an archive of metadata files validated and zipped by @@ -625,8 +633,6 @@ def _expand_metadata_archive(self, metadata_archive_fname): - - def validate_image(self, image_fname): """ Determines if the image with filename provided matches the expected file From cd03e8a2cf69ebfd0175fc1150c3e0e506fa8d03 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Tue, 2 Jan 2018 16:58:22 -0500 Subject: [PATCH 03/43] Fixed a bug that raised error in json mode --- demo/demo_secondary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index 90c05ac..4c7bbbf 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -340,6 +340,7 @@ def update_cycle(): secondary_ecu.full_client_dir, 'director_targets.'+tuf.conf.METADATA_FORMAT) with open(director_targets_role, 'wb') as f: f.write(metadata_from_primary.data) + secondary_ecu.process_metadata(director_targets_role) else: archive_fname = os.path.join( secondary_ecu.full_client_dir, 'metadata_from_primary.zip') @@ -349,7 +350,7 @@ 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) + secondary_ecu.process_metadata(archive_fname) # As part of the process_metadata call, the secondary will have saved From 61fb1767807fdc862bb10cdf4649f9c4ed91560d Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Sat, 6 Jan 2018 11:36:51 -0500 Subject: [PATCH 04/43] Changes made to make the targets metadata be verified using the director's key Uses uptane.common.verify_signature_over_metadata --- uptane/clients/secondary.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 7f473af..3cbfb6f 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -573,34 +573,47 @@ def process_partial_metadata(self, director_targets_metadata): """ Implementation for partial verification in secondaries. Accepts the director targets metadata. - Processes it to only get a tuf.util.tempfile to hold the director - target's metadata. Checks the targets metadata for the different conditions and attacks as specified in the implementation requirements. Performs partial validation and stores the directors target info - recieved for a validated target (hash, length, etc.). + recieved for a validated target (hash, length, etc.) for the particular + ecu. """ tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) + if self.director_public_key is None: + raise uptane.Error("Director public key not found for partial" + " verification of secondary.") validated_targets_for_this_ecu = [] target_metadata = {} if not os.path.exists(director_targets_metadata): raise uptane.Error('Indicated metadata archive does not exist. ' 'Filename: ' + repr(director_targets_metadata)) + metadata_file_object = tuf.util.load_file(director_targets_metadata) - metadata_file_object = tuf.util.TempFile() - with open(director_targets_metadata, 'rb') as f: - data = f.read() - metadata_file_object.write(data) - self.updater.repositories[self.director_repo_name].\ - _verify_uncompressed_metadata_file(metadata_file_object, 'targets') - - data = tuf.util.load_file(director_targets_metadata) - targets = data['signed']['targets'] - for file in targets.keys(): - target_metadata['filepath'] = file - target_metadata['fileinfo'] = targets[file] - validated_targets_for_this_ecu.append(target_metadata) + valid = uptane.common.verify_signature_over_metadata( + self.director_public_key, + metadata_file_object['signatures'][0], # TODO: Fix single-signature assumption + metadata_file_object['signed'], + datatype='ecu_manifest') + + if not valid: + log.info( + 'Validation failed on an director targets: signature is not valid. ' + 'It must be correctly signed by the expected key for that ECU.') + raise tuf.BadSignatureError('Sender supplied an invalid signature. ' + 'Director targets metadata is unacceptable. If you see this ' + 'persistently, it is possible that the Primary is compromised or ' + 'that there is a man in the middle attack or misconfiguration.') + + targets = metadata_file_object['signed']['targets'] + #Combs through the director's targets metadata to find the one assigned to the + #current ECU. + for target in targets: + if targets[target]['custom']['ecu_serial'] == self.ecu_serial: + target_metadata['filepath'] = target + target_metadata['fileinfo'] = targets[target] + validated_targets_for_this_ecu.append(target_metadata) self.validated_targets_for_this_ecu = validated_targets_for_this_ecu From 376eaa89569105aaefd19db237294cfff6e698ab Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Wed, 10 Jan 2018 18:37:47 -0500 Subject: [PATCH 05/43] - Added test for PV Secondaries : test_45_process_partial_metadata - Changed error name for director public key being None in secondary.py to tuf.UnknownKeyError - Added sample metadata for PV secondaries. Both correspond to ECUs with vin=111, ecu_serial=20000. One has a valid signature the other one doesn't as specified in the name of the samples --- .../sample_pv_secondary_target_metadata.json | 30 ++++++ ..._pv_secondary_target_metadata_bad_sig.json | 30 ++++++ tests/test_secondary.py | 99 ++++++++++++++++--- uptane/clients/secondary.py | 5 +- 4 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 samples/sample_pv_secondary_target_metadata.json create mode 100644 samples/sample_pv_secondary_target_metadata_bad_sig.json diff --git a/samples/sample_pv_secondary_target_metadata.json b/samples/sample_pv_secondary_target_metadata.json new file mode 100644 index 0000000..ba5acea --- /dev/null +++ b/samples/sample_pv_secondary_target_metadata.json @@ -0,0 +1,30 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "9560312be8ebd451c689724a3ec2fcbdb7ba708b27669861e0146bd61a2a15f04baf90edc75edd2e437ac680a5d2b3d76b3e4b9766a8a961108af6b4e501950e" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2021-01-10T15:14:21Z", + "targets": { + "/firmware13.img": { + "custom": { + "ecu_serial": "20000" + }, + "hashes": { + "sha256": "daeec2555599b8e7a82b6f1339d5f419346b57a0eb7a39ee6334b8f205595752", + "sha512": "1570937a84e9e74e35f5e56a8f8518c91e18258cffc8ace249d9acf173d1845d82002583b1b53373b79edf52494d524f2619f5f1896f3085038deca92c950486" + }, + "length": 20 + } + }, + "version": 2 + } +} diff --git a/samples/sample_pv_secondary_target_metadata_bad_sig.json b/samples/sample_pv_secondary_target_metadata_bad_sig.json new file mode 100644 index 0000000..cb92e95 --- /dev/null +++ b/samples/sample_pv_secondary_target_metadata_bad_sig.json @@ -0,0 +1,30 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "a560312be8ebd451c689724a3ec2fcbdb7ba708b27669861e0146bd61a2a15f04baf90edc75edd2e437ac680a5d2b3d76b3e4b9766a8a961108af6b4e501950e" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2021-01-10T15:14:21Z", + "targets": { + "/firmware13.img": { + "custom": { + "ecu_serial": "20000" + }, + "hashes": { + "sha256": "daeec2555599b8e7a82b6f1339d5f419346b57a0eb7a39ee6334b8f205595752", + "sha512": "1570937a84e9e74e35f5e56a8f8518c91e18258cffc8ace249d9acf173d1845d82002583b1b53373b79edf52494d524f2619f5f1896f3085038deca92c950486" + }, + "length": 20 + } + }, + "version": 2 + } +} diff --git a/tests/test_secondary.py b/tests/test_secondary.py index ab2abe1..702491e 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -50,17 +50,25 @@ TEMP_CLIENT_DIRS = [ os.path.join(TEST_DATA_DIR, 'temp_test_secondary0'), os.path.join(TEST_DATA_DIR, 'temp_test_secondary1'), - os.path.join(TEST_DATA_DIR, 'temp_test_secondary2')] + os.path.join(TEST_DATA_DIR, 'temp_test_secondary2'), + os.path.join(TEST_DATA_DIR, 'temp_test_partial_secondary0')] + +NUM_PARTIAL_SECONDARIES = 1 +# Indices of PV Secondaries in the list of secondaries +PV_SECONDARY1_INDICE = 3 +# Assigns the last X number of secondaries as partial verification # I'll initialize these in the __init__ test, and use this for the simple # non-damaging tests so as to avoid creating objects all over again. -secondary_instances = [None, None, None] +# Initializes the number of secondary instances to the number of +# TEMP_CLIENT_DIRS +secondary_instances = [None] * len(TEMP_CLIENT_DIRS) # Changing these values would require producing new signed test data from the # Timeserver (in the case of nonce) or a Secondary (in the case of the others). nonce = 5 -vins = ['democar', 'democar', '000'] -ecu_serials = ['TCUdemocar', '00000', '00000'] +vins = ['democar', 'democar', '000', '111', '111'] +ecu_serials = ['TCUdemocar', '00000', '00000', '20000', '30000'] # Set starting firmware fileinfo (that this ECU had coming from the factory) # It will serve as the initial firmware state for the Secondary clients. @@ -360,7 +368,13 @@ def test_01_init(self): client_dir = TEMP_CLIENT_DIRS[i] ecu_serial = ecu_serials[i] vin = vins[i] - + partial_verifying_for_ecu = False + director_public_key_for_ecu = None + # Sets the last NUM_PARTIAL_SECONDARIES number of ECUs to partial + # verifying + if i == PV_SECONDARY1_INDICE: + partial_verifying_for_ecu = True + director_public_key_for_ecu = self.key_directortargets_pub # Try initializing each of three secondaries, expecting these calls to # work. Save the instances for future tests as elements in a module list # variable(secondary_instances) to save time and code. @@ -374,9 +388,8 @@ def test_01_init(self): time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, firmware_fileinfo=factory_firmware_fileinfo, - director_public_key=None, - partial_verifying=False) - + director_public_key=director_public_key_for_ecu, + partial_verifying=partial_verifying_for_ecu) instance = secondary_instances[i] # Check the fields initialized in the instance to make sure they're correct. @@ -393,8 +406,16 @@ def test_01_init(self): TestSecondary.initial_time, instance.all_valid_timeserver_times[1]) self.assertEqual( TestSecondary.key_timeserver_pub, instance.timeserver_public_key) - self.assertTrue(None is instance.director_public_key) - self.assertFalse(instance.partial_verifying) + + + #Checks the number of secondaries for PV + if i == PV_SECONDARY1_INDICE: + self.assertTrue(instance.partial_verifying) + self.assertFalse(None is instance.director_public_key) + else: + self.assertFalse(instance.partial_verifying) + self.assertTrue(None is instance.director_public_key) + # Fields initialized, but not directly with parameters self.assertTrue(None is instance.last_nonce_sent) @@ -626,7 +647,8 @@ def test_40_process_metadata(self): # Continue set-up followed by the test, per client. - for i in range(0, len(TEMP_CLIENT_DIRS)): + # Only tests the full verification secondaries + for i in range(0, len(TEMP_CLIENT_DIRS)-NUM_PARTIAL_SECONDARIES): client_dir = TEMP_CLIENT_DIRS[i] instance = secondary_instances[i] @@ -710,6 +732,61 @@ def test_40_process_metadata(self): + def test_45_process_partial_metadata(self): + """ + Tests uptane.clients.secondary.Secondary::process_partial_metadata() + + Tests PV Secondary client. + - secondary_instances[3]: Director's targets metadata available with + valid signatures + - secondary_instances[3]: Director's targets metadata available with + invalid signatures + """ + # --- Test this test module's setup (defensive) + # First, check the source directories, from which the temp dir is copied. + # This first part is testing this test module, since this setup was done + # above in setUpClass(), to maintain test integrity over time. + # We should see only root.(json or der). + for data_directory in [ + TEST_DIRECTOR_METADATA_DIR, TEST_IMAGE_REPO_METADATA_DIR]: + + self.assertEqual( + ['root.der', 'root.json'], + sorted(os.listdir(data_directory))) + + sample_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', + 'sample_pv_secondary_target_metadata.json') + + sample_bad_sig_working_metadata_path = os.path.join(uptane.WORKING_DIR, + 'samples', 'sample_pv_secondary_target_metadata_bad_sig.json') + + pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] + instance = secondary_instances[PV_SECONDARY1_INDICE] + director_targets_metadata_path = os.path.join(pv_secondary_dir, + 'metadata', 'director_targets.json') + + + # PV Secondary 1 with valid director public key and update + shutil.copy(sample_working_metadata_path, director_targets_metadata_path) + if not os.path.exists(director_targets_metadata_path): + raise("Director's targets not available") + + instance.process_metadata(director_targets_metadata_path) + + # PV Secondary 1 with valid director public key but update with + # invalid signature. + shutil.copy(sample_bad_sig_working_metadata_path, director_targets_metadata_path) + if not os.path.exists(director_targets_metadata_path): + raise("Director's targets not available") + with self.assertRaises(tuf.BadSignatureError): + instance.process_metadata(director_targets_metadata_path) + + + + + + + def test_50_validate_image(self): image_fname = 'TCU1.1.txt' diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 3cbfb6f..d52aaad 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -581,11 +581,12 @@ def process_partial_metadata(self, director_targets_metadata): """ tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) + # Checks if the secondary holds the director's public key if self.director_public_key is None: - raise uptane.Error("Director public key not found for partial" + raise tuf.UnknownKeyError("Director public key not found for partial" " verification of secondary.") validated_targets_for_this_ecu = [] - target_metadata = {} + target_metadata = {} if not os.path.exists(director_targets_metadata): raise uptane.Error('Indicated metadata archive does not exist. ' 'Filename: ' + repr(director_targets_metadata)) From ca9e58323664267b3ad389af6e14da1c58d6fa78 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Wed, 10 Jan 2018 18:39:06 -0500 Subject: [PATCH 06/43] Removed some left over depracated changes --- tests/test_secondary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 702491e..584e6ae 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -67,8 +67,8 @@ # Changing these values would require producing new signed test data from the # Timeserver (in the case of nonce) or a Secondary (in the case of the others). nonce = 5 -vins = ['democar', 'democar', '000', '111', '111'] -ecu_serials = ['TCUdemocar', '00000', '00000', '20000', '30000'] +vins = ['democar', 'democar', '000', '111'] +ecu_serials = ['TCUdemocar', '00000', '00000', '20000'] # Set starting firmware fileinfo (that this ECU had coming from the factory) # It will serve as the initial firmware state for the Secondary clients. From 6669b1154e97793ecb6b75edec5554dc537c2764 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Thu, 11 Jan 2018 02:15:57 -0500 Subject: [PATCH 07/43] Fixing bug that stops the PV secondaries from working with .der format Fixed ASN1 bug --- uptane/clients/secondary.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index d52aaad..d68d31c 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -37,7 +37,8 @@ import uptane.formats import uptane.common -import uptane.encoding.asn1_codec as asn1_codec +import uptane.encoding.asn1_codec as asn1_codec #Uptane name TODO +import tuf.asn1_codec as tuf_asn1_codec from uptane.encoding.asn1_codec import DATATYPE_TIME_ATTESTATION from uptane.encoding.asn1_codec import DATATYPE_ECU_MANIFEST @@ -583,20 +584,34 @@ def process_partial_metadata(self, director_targets_metadata): tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) # Checks if the secondary holds the director's public key if self.director_public_key is None: - raise tuf.UnknownKeyError("Director public key not found for partial" + raise uptane.Error("Director public key not found for partial" " verification of secondary.") validated_targets_for_this_ecu = [] target_metadata = {} if not os.path.exists(director_targets_metadata): raise uptane.Error('Indicated metadata archive does not exist. ' 'Filename: ' + repr(director_targets_metadata)) + + metadata_file_object = tuf.util.load_file(director_targets_metadata) - valid = uptane.common.verify_signature_over_metadata( - self.director_public_key, - metadata_file_object['signatures'][0], # TODO: Fix single-signature assumption - metadata_file_object['signed'], - datatype='ecu_manifest') + data = metadata_file_object['signed'] + signature = metadata_file_object['signatures'][0] + + if director_targets_metadata.endswith('json'): + data = tuf.formats.encode_canonical(data).encode('utf-8') + + elif director_targets_metadata.endswith('der'): + data = tuf_asn1_codec.convert_signed_metadata_to_der( + {'signed': data, 'signatures': []}, only_signed=True) + data = hashlib.sha256(data).digest() + + else: # pragma: no cover + raise uptane.Error('Unsupported metadata format: ' + repr(metadata_format) + + '; the supported formats are: "der" and "json".') + + valid = tuf.keys.verify_signature(self.director_public_key, + signature, data) if not valid: log.info( From bb755e87745e771691865867550ae965bc09e683 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Tue, 16 Jan 2018 15:06:28 -0500 Subject: [PATCH 08/43] Added instructions and details about PV secondaries in README.md --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0409cb..0aa04f3 100644 --- a/README.md +++ b/README.md @@ -180,25 +180,38 @@ with information about the "firmware" that it is running, which we send to the Primary. Open a Python shell in a new terminal window and then run the following: - +- For full verification secondaries ```python >>> import demo.demo_secondary as ds >>> ds.clean_slate() >>> ds.update_cycle() ``` +- For partial verification secondaries +```python +>>> import demo.demo_secondary as ds +>>> ds.clean_slate(partial_verifying=True) +>>> ds.update_cycle() +``` -Optionally, multiple windows with different Secondary clients can be run simultaneously. In each additional window, you can run the same calls as above to set up a new ECU in the same, default vehicle by modifying the clean_slate() call to include a distinct ECU Serial. e.g. `ds.clean_slate(ecu_serial='33333')` +Optionally, multiple windows with different Secondary clients can be run simultaneously. In each additional window, you can run the same calls as above to set up a new ECU in the same, default vehicle by modifying the clean_slate() call to include a distinct ECU Serial. e.g. `ds.clean_slate(ecu_serial='33333')` for Full Verification Secondaries or `ds.clean_slate(partial_verifying=True, ecu_serial='33333')` for Partial Verifying Secondaries. If the Secondary is in a different vehicle from the default vehicle, this call should look like: `ds.clean_slate(vin='vehicle_id_here', ecu_serial='ecu_serial_here', primary_port=30702)`, providing a VIN for the new vehicle, a unique ECU Serial, and indicating the port listed by this Secondary's Primary when that Primary initialized (e.g. "Primary will now listen on port 30702"). -The Secondary's update_cycle() call: +The Secondary's update_cycle() call for Full Verification Secondaries: - fetches and validates the signed metadata for the vehicle from the Primary - fetches any image that the Primary assigns to this ECU, validating that against the instructions of the Director in the Director's metadata, and against file info available in the Image Repository's metadata. If the image from the Primary does not match validated metadata, it is discarded. - fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver and the signature checks out, this time is saved as valid and reasonably recent. - generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) - submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) +The Secondary's update_cycle() call for Partial Verification Secondaries: +- fetches and validates the signed director's targets metadata for the vehicle from the Primary +- fetches any image that the Primary assigns to this ECU, validating that against the instructions of the Director in the Director's metadata. If the image from the Primary does not match validated metadata, it is discarded. +- fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver and the signature checks out, this time is saved as valid and reasonably recent. +- generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) +- submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) + # 2: Delivering an Update From a5888236bcfc041fe699d3fac2c3dd48cbf7d56c Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Tue, 16 Jan 2018 15:26:19 -0500 Subject: [PATCH 09/43] Fixed some style issues in the test_secondary.py --- tests/test_secondary.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 584e6ae..6d8f648 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -53,10 +53,11 @@ os.path.join(TEST_DATA_DIR, 'temp_test_secondary2'), os.path.join(TEST_DATA_DIR, 'temp_test_partial_secondary0')] +# Assigns the last NUM_PARTIAL_SECONDARIES number of +# secondaries as partial verification NUM_PARTIAL_SECONDARIES = 1 # Indices of PV Secondaries in the list of secondaries PV_SECONDARY1_INDICE = 3 -# Assigns the last X number of secondaries as partial verification # I'll initialize these in the __init__ test, and use this for the simple # non-damaging tests so as to avoid creating objects all over again. @@ -736,7 +737,7 @@ def test_45_process_partial_metadata(self): """ Tests uptane.clients.secondary.Secondary::process_partial_metadata() - Tests PV Secondary client. + Tests PV Secondary client in 2 situations: - secondary_instances[3]: Director's targets metadata available with valid signatures - secondary_instances[3]: Director's targets metadata available with @@ -770,7 +771,6 @@ def test_45_process_partial_metadata(self): shutil.copy(sample_working_metadata_path, director_targets_metadata_path) if not os.path.exists(director_targets_metadata_path): raise("Director's targets not available") - instance.process_metadata(director_targets_metadata_path) # PV Secondary 1 with valid director public key but update with @@ -785,8 +785,6 @@ def test_45_process_partial_metadata(self): - - def test_50_validate_image(self): image_fname = 'TCU1.1.txt' From 941dab1bc505308eb9c65eaf855efb3546996a1e Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Tue, 16 Jan 2018 15:59:00 -0500 Subject: [PATCH 10/43] Made variable name changes for uptane_asn1_codec and tuf_asn1_codec in secondary.py --- uptane/clients/secondary.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index d68d31c..1a1885a 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -37,7 +37,7 @@ import uptane.formats import uptane.common -import uptane.encoding.asn1_codec as asn1_codec #Uptane name TODO +import uptane.encoding.asn1_codec as uptane_asn1_codec import tuf.asn1_codec as tuf_asn1_codec from uptane.encoding.asn1_codec import DATATYPE_TIME_ATTESTATION @@ -363,7 +363,7 @@ def generate_signed_ecu_manifest(self, description_of_attacks_observed=''): signable_ecu_manifest) if tuf.conf.METADATA_FORMAT == 'der': - der_signed_ecu_manifest = asn1_codec.convert_signed_metadata_to_der( + der_signed_ecu_manifest = uptane_asn1_codec.convert_signed_metadata_to_der( signable_ecu_manifest, DATATYPE_ECU_MANIFEST, resign=True, private_key=self.ecu_key) # TODO: Consider verification of output here. @@ -394,7 +394,7 @@ def validate_time_attestation(self, timeserver_attestation): # If we're using ASN.1/DER format, convert the attestation into something # comprehensible (JSON-compatible dictionary) instead. if tuf.conf.METADATA_FORMAT == 'der': - timeserver_attestation = asn1_codec.convert_signed_der_to_dersigned_json( + timeserver_attestation = uptane_asn1_codec.convert_signed_der_to_dersigned_json( timeserver_attestation, DATATYPE_TIME_ATTESTATION) # Check format. @@ -591,7 +591,6 @@ def process_partial_metadata(self, director_targets_metadata): if not os.path.exists(director_targets_metadata): raise uptane.Error('Indicated metadata archive does not exist. ' 'Filename: ' + repr(director_targets_metadata)) - metadata_file_object = tuf.util.load_file(director_targets_metadata) @@ -623,6 +622,7 @@ def process_partial_metadata(self, director_targets_metadata): 'that there is a man in the middle attack or misconfiguration.') targets = metadata_file_object['signed']['targets'] + #Combs through the director's targets metadata to find the one assigned to the #current ECU. for target in targets: @@ -630,6 +630,7 @@ def process_partial_metadata(self, director_targets_metadata): target_metadata['filepath'] = target target_metadata['fileinfo'] = targets[target] validated_targets_for_this_ecu.append(target_metadata) + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu From 18b431dd720226aafffa187ce7eca7b487c44b76 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Wed, 17 Jan 2018 16:37:27 -0500 Subject: [PATCH 11/43] More testing for secondary.py | process_partial_metadata() --- tests/test_secondary.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 6d8f648..67e5043 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -739,9 +739,11 @@ def test_45_process_partial_metadata(self): Tests PV Secondary client in 2 situations: - secondary_instances[3]: Director's targets metadata available with - valid signatures + valid signatures in JSON mode - secondary_instances[3]: Director's targets metadata available with - invalid signatures + invalid signatures in JSON mode + - secondary_instances[3]: Director's targets metadata available with + valid signatures in DER mode """ # --- Test this test module's setup (defensive) # First, check the source directories, from which the temp dir is copied. @@ -761,25 +763,39 @@ def test_45_process_partial_metadata(self): sample_bad_sig_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', 'sample_pv_secondary_target_metadata_bad_sig.json') + sample_der_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', + 'sample_pv_secondary_target_metadata.der') + pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] instance = secondary_instances[PV_SECONDARY1_INDICE] - director_targets_metadata_path = os.path.join(pv_secondary_dir, + director_targets_metadata_path_json = os.path.join(pv_secondary_dir, 'metadata', 'director_targets.json') + director_targets_metadata_path_der = os.path.join(pv_secondary_dir, + 'metadata', 'director_targets.der') - # PV Secondary 1 with valid director public key and update - shutil.copy(sample_working_metadata_path, director_targets_metadata_path) - if not os.path.exists(director_targets_metadata_path): + # PV Secondary 1 with valid director public key and update JSON + shutil.copy(sample_working_metadata_path, director_targets_metadata_path_json) + if not os.path.exists(director_targets_metadata_path_json): raise("Director's targets not available") - instance.process_metadata(director_targets_metadata_path) + instance.process_metadata(director_targets_metadata_path_json) # PV Secondary 1 with valid director public key but update with - # invalid signature. - shutil.copy(sample_bad_sig_working_metadata_path, director_targets_metadata_path) - if not os.path.exists(director_targets_metadata_path): + # invalid signature. JSON + shutil.copy(sample_bad_sig_working_metadata_path, + director_targets_metadata_path_json) + if not os.path.exists(director_targets_metadata_path_json): raise("Director's targets not available") with self.assertRaises(tuf.BadSignatureError): - instance.process_metadata(director_targets_metadata_path) + instance.process_metadata(director_targets_metadata_path_json) + + # PV Secondary 1 with valid director public key and update DER + shutil.copy(sample_der_working_metadata_path, + director_targets_metadata_path_der) + if not os.path.exists(director_targets_metadata_path_der): + raise("Director's targets not available") + instance.process_metadata(director_targets_metadata_path_der) + From edcf05ec7f8f142c6285972889d2933816d9c1f6 Mon Sep 17 00:00:00 2001 From: Shikhar Sakhuja Date: Wed, 17 Jan 2018 16:38:12 -0500 Subject: [PATCH 12/43] Sample metadata in DER format to support more tests added --- samples/sample_pv_secondary_target_metadata.der | Bin 0 -> 299 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 samples/sample_pv_secondary_target_metadata.der diff --git a/samples/sample_pv_secondary_target_metadata.der b/samples/sample_pv_secondary_target_metadata.der new file mode 100644 index 0000000000000000000000000000000000000000..a5ecd0936a63e29fd534e44d9ab45ab62c19cd23 GIT binary patch literal 299 zcmV+`0o495f&nL>fwF)B0f7Ws*c;)30Rp3eqoIMIfB^xaftoOZm!N@*fDbQbX>x6M zVRB_MGcIXuXMq6}f&l`faxf==0Rf>RfgsxM!c|q7xaX)VZxcDy^cgg3SD@>9IqqXL zxbg*AS5h!XfB^%cLV-XPaFcq3>E}*0_2p`hg&4^m7$uDV$gJW?*{tz%(S%)s040O5 zwKH?Kp5IbQO;S%L8TIjrZ!m=ejqIr`l>~;N2Z9ALFfcGMfdK)cZ7^$~B7q=d4E2Qb zl0yq28KgO0Ibu$R_2tDkMF*#ezFhNL@Q~Jl0Rf^yfj~J5(BHoLvTn($mL_qat>P`F x_?>v%hFx%q*3gY7+y2L+dLSZdMSrY;n?@hRkBaFAj>&_R#H-J{iT?}M#tf!Eh%x{G literal 0 HcmV?d00001 From d24ffded1bcf8389c3b34ca3f5e7e120875a8ee2 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:13:55 -0500 Subject: [PATCH 13/43] Remove trailing whitespace in partial-verification secondary code Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 8 ++++---- uptane/clients/secondary.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 67e5043..0a72779 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -407,10 +407,10 @@ def test_01_init(self): TestSecondary.initial_time, instance.all_valid_timeserver_times[1]) self.assertEqual( TestSecondary.key_timeserver_pub, instance.timeserver_public_key) - - - #Checks the number of secondaries for PV - if i == PV_SECONDARY1_INDICE: + + + #Checks the number of secondaries for PV + if i == PV_SECONDARY1_INDICE: self.assertTrue(instance.partial_verifying) self.assertFalse(None is instance.director_public_key) else: diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 1a1885a..8d31da6 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -37,7 +37,7 @@ import uptane.formats import uptane.common -import uptane.encoding.asn1_codec as uptane_asn1_codec +import uptane.encoding.asn1_codec as uptane_asn1_codec import tuf.asn1_codec as tuf_asn1_codec from uptane.encoding.asn1_codec import DATATYPE_TIME_ATTESTATION From 5780258fd7f8ba7d497ccfcf6871211307c7674a Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:15:07 -0500 Subject: [PATCH 14/43] Correct assertions (style / function use) in test_secondary Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 0a72779..d5d1afa 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -412,14 +412,14 @@ def test_01_init(self): #Checks the number of secondaries for PV if i == PV_SECONDARY1_INDICE: self.assertTrue(instance.partial_verifying) - self.assertFalse(None is instance.director_public_key) + self.assertIsNotNone(instance.director_public_key) else: self.assertFalse(instance.partial_verifying) - self.assertTrue(None is instance.director_public_key) + self.assertIsNone(instance.director_public_key) # Fields initialized, but not directly with parameters - self.assertTrue(None is instance.last_nonce_sent) + self.assertIsNone(instance.last_nonce_sent) self.assertTrue(instance.nonce_next) # Random value self.assertIsInstance( instance.updater, tuf.client.updater.Updater) From e5c73be2776efaf16f71b52c3e84892257b3032b Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:19:34 -0500 Subject: [PATCH 15/43] DOC: Improve docstrings and comments in secondary.py Functions have become slightly more complex due to the addition of partial verification client support in the reference implementation, so the documentation had to be improved. Note that some of the procedures and variables described are slightly different than what the docstring suggests: the changes to functions in the next commit will bring them in line. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 136 ++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 23 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 8d31da6..fce4a9c 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -3,20 +3,31 @@ secondary.py - Provides core functionality for Uptane Secondary ECU clients: - - Given an archive of metadata and an image file, performs full verification - of both, employing TUF (The Update Framework), determining if this + Provides core functionality for Uptane Secondary ECU clients. A detailed + explanation of the role of the Secondary in Uptane is available in the + "Design Overview" and "Implementation Specification" documents, links to + which are maintained at uptane.github.io + + Note that while this implementation uses files and archives, neither archives + nor a filesystem are key to the system and the same algorithms can be + employed regardless. + + Briefly, the Secondary code here does the following: + + - Given an archive of metadata (or in the case of a partial verifier, the + Director's Targets role file), performs full or partial verification of + both, employing TUF (The Update Framework), determining if this Secondary ECU has been instructed to install the image by the Director and - if the image is also valid per the Image Repository. + (for full verification) if the image is also valid per the Image Repository. + Core metadata validation functionality is provided by the function + process_metadata(), whose docstring describes the metadata validation. + - Generates ECU Manifests describing the state of the Secondary for Director perusal + - Generates nonces for time requests from the Timeserver, and validates signed times provided by the Timeserver, maintaining trustworthy times. Rotates nonces after they have appeared in Timeserver responses. - - A detailed explanation of the role of the Secondary in Uptane is available in - the "Design Overview" and "Implementation Specification" documents, links to - which are maintained at uptane.github.io """ from __future__ import print_function from __future__ import unicode_literals @@ -550,11 +561,52 @@ def get_validated_target_info(self, target_filepath): def process_metadata(self, metadata_archive_fname): """ - Expand the metadata archive using _expand_metadata_archive() - Validate metadata files using fully_validate_metadata() - Select the Director targets.json file - Pick out the target file(s) with our ECU serial listed - Fully validate the metadata for the target file(s) + Runs either partial or full metadata verification, based on the + value of self.partial_verifying. + + Note that in both cases, the use of files and archives is not key. Keep an + eye on the procedure without regard to them. The central idea is to take + the metadata pointed at by the argument here as untrusted and verify it + using the full verification or partial verification algorithms from the + Uptane Implementation Specification. It's generally expected that this + metadata comes to the Secondary from the Primary, originally from the + Director and Image repositories, but the way it gets here does not matter + as long as it checks out as trustworthy. + + Full: + The given filename, metadata_fname, should point to an archive of all + metadata necessary to perform full verification, such as is produced by + primary.save_distributable_metadata_files(). + + process_metadata expands this archive to a local directory where + repository files are expected to be found (the 'unverified' directory in + directory self.full_client_dir). + + Then, these expanded metadata files are treated as repository metadata by + the call to fully_validate_metadata(). The Director targets.json file is + selected. The target file(s) with this Secondary's ECU serial listed is + fully validated, using whatever provided metadata is necessary, by the + underlying TUF code. + + Partial: + + The given filename, metadata_fname, should point to a single metadata + role file, the Director's Targets role. The signature on the Targets role + file is validated against the Director's public key + (self.director_public_key). If the signature is valid, the new Targets + role file is trusted, else it is discarded and we TUF raises a + signature exception. + + (Additional protections come from the Primary + having vetted the file for us using full verification, as long as the + Primary is trustworthy.) + + From the trusted Targets role file, the target with this Secondary's + ECU identifier/serial listed is chosen, and the metadata describing that + target (hash, length, etc.) is extracted from the metadata file and + taken as the trustworthy description of the targets file to be installed + on this Secondary. + """ tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) if self.partial_verifying: @@ -572,13 +624,51 @@ def process_metadata(self, metadata_archive_fname): def process_partial_metadata(self, director_targets_metadata): """ - Implementation for partial verification in secondaries. - Accepts the director targets metadata. - Checks the targets metadata for the different conditions and attacks as - specified in the implementation requirements. - Performs partial validation and stores the directors target info - recieved for a validated target (hash, length, etc.) for the particular - ecu. + + Given the filename of a file containing the Director's Targets role + metadata, validates and processes that metadata, determining what firmware + the Director has instructed this partial-verification Secondary ECU to + install. + + The given metadata replaces this client's current Director metadata if + the given metadata is valid -- i.e. if the metadata: + - is signed by a key matching self.director_public_key + - and is not expired (current date is before metadata's expiration date) + - and does not have an older version number than this client has + previously seen -- i.e. is not a rollback) + + Otherwise, an exception is raised indicating that the metadata is not + valid. + + Further, if the metadata is valid, this function then updates + self.validated_target_for_this_ecu if the metadata also lists a target + for this ECU (i.e. includes a target with field "ecu_serial" set to this + ECU's serial number) + + + + director_targets_metadata_fname + Filename of the Director's Targets role metadata, in either JSON or + ASN.1/DER format. + + + None + + + tuf.BadSignatureError + if the signature over the Targets metadata is not a valid + signature by the key corresponding to self.director_public_key. + + tuf.ExpiredMetadataError + if the Targets metadata is expired + + tuf.ReplayedMetadataError + if the Targets metadata has a lower version number than + the last Targets metadata this client deemed valid (rollback) + + + May update this client's metadata (Director Targets); see + May update self.validated_targets_for_this_ecu; see """ tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) @@ -623,9 +713,9 @@ def process_partial_metadata(self, director_targets_metadata): targets = metadata_file_object['signed']['targets'] - #Combs through the director's targets metadata to find the one assigned to the - #current ECU. - for target in targets: + # Combs through the director's targets metadata to find the one assigned to + # the current ECU. + for target in targets: if targets[target]['custom']['ecu_serial'] == self.ecu_serial: target_metadata['filepath'] = target target_metadata['fileinfo'] = targets[target] From fd347118e4a050cb94a62dc6f0ae52c058dc35ed Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:23:38 -0500 Subject: [PATCH 16/43] Update variable names for p-v secondaries (readability) Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index fce4a9c..acf6141 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -559,7 +559,7 @@ def get_validated_target_info(self, target_filepath): - def process_metadata(self, metadata_archive_fname): + def process_metadata(self, metadata_fname): """ Runs either partial or full metadata verification, based on the value of self.partial_verifying. @@ -608,21 +608,20 @@ def process_metadata(self, metadata_archive_fname): on this Secondary. """ - tuf.formats.RELPATH_SCHEMA.check_match(metadata_archive_fname) + tuf.formats.RELPATH_SCHEMA.check_match(metadata_fname) + if self.partial_verifying: - self.process_partial_metadata(metadata_archive_fname) + self.process_partial_metadata(metadata_fname) else: - self._expand_metadata_archive(metadata_archive_fname) - - # This entails using the local metadata files as a repository. + self._expand_metadata_archive(metadata_fname) self.fully_validate_metadata() - def process_partial_metadata(self, director_targets_metadata): + def process_partial_metadata(self, director_targets_metadata_fname): """ Given the filename of a file containing the Director's Targets role @@ -671,18 +670,18 @@ def process_partial_metadata(self, director_targets_metadata): May update self.validated_targets_for_this_ecu; see """ - tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata) + tuf.formats.RELPATH_SCHEMA.check_match(director_targets_metadata_fname) # Checks if the secondary holds the director's public key if self.director_public_key is None: raise uptane.Error("Director public key not found for partial" " verification of secondary.") validated_targets_for_this_ecu = [] - target_metadata = {} - if not os.path.exists(director_targets_metadata): + target_metadata = {} + if not os.path.exists(director_targets_metadata_fname): raise uptane.Error('Indicated metadata archive does not exist. ' - 'Filename: ' + repr(director_targets_metadata)) - - metadata_file_object = tuf.util.load_file(director_targets_metadata) + 'Filename: ' + repr(director_targets_metadata_fname)) + + metadata_file_object = tuf.util.load_file(director_targets_metadata_fname) data = metadata_file_object['signed'] signature = metadata_file_object['signatures'][0] From 585e2b3cbbb9e8b37fe4557123fb4e1d1a8f6ab5 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:25:42 -0500 Subject: [PATCH 17/43] Add rollback attack protection to new partial verification secondary code Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index acf6141..4089943 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -272,6 +272,11 @@ def __init__( 'key was not provided. Partial verification Secondaries validate ' 'only the ') + # Partial verification clients still have to track the last valid metadata + # version number. (For full verification clients, the TUF updater code + # handles this.) + if self.partial_verifying: + self.last_valid_targets_metadata_version_number = 0 # Create a TAP-4-compliant updater object. This will read pinned.json # and create single-repository updaters within it to handle connections to @@ -691,6 +696,13 @@ def process_partial_metadata(self, director_targets_metadata_fname): elif director_targets_metadata.endswith('der'): data = tuf_asn1_codec.convert_signed_metadata_to_der( + + # Check to see if the metadata has a lower version number than expected. + if data['version'] < self.last_valid_targets_metadata_version_number: + raise tuf.ReplayedMetadataError('Provided metadata has lower version ' + 'number than the metadata this partial verification Secondary has ' + 'previously validated.') + {'signed': data, 'signatures': []}, only_signed=True) data = hashlib.sha256(data).digest() @@ -709,7 +721,10 @@ def process_partial_metadata(self, director_targets_metadata_fname): 'Director targets metadata is unacceptable. If you see this ' 'persistently, it is possible that the Primary is compromised or ' 'that there is a man in the middle attack or misconfiguration.') - + + # The metadata is valid, so we update the last validated version number + # and begin inspecting the targets listed in the metadata. + self.last_valid_targets_metadata_version_number = data['version'] targets = metadata_file_object['signed']['targets'] # Combs through the director's targets metadata to find the one assigned to From 7e250b742ba254e5afac2e727086236d65bb43dd Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:26:46 -0500 Subject: [PATCH 18/43] Add metadata expiration check to partial verification secondary code Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 4089943..998778d 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -696,6 +696,17 @@ def process_partial_metadata(self, director_targets_metadata_fname): elif director_targets_metadata.endswith('der'): data = tuf_asn1_codec.convert_signed_metadata_to_der( + # Check to see if the metadata is expired. + last_timeserver_time = tuf.formats.datetime_to_unix_timestamp( + self.all_valid_timeserver_times[-1]) + expiration_time = tuf.formats.datetime_to_unix_timestamp(data['expires']) + + if expiration_time < last_timeserver_time: + raise tuf.ExpiredMetadataError('Expired metadata provided to partial ' + 'verification Secondary; last valid timeserver time: ' + + self.all_valid_timeserver_times[-1] + '; expiration date on ' + 'metadata: ' + data['expires']) + # Check to see if the metadata has a lower version number than expected. if data['version'] < self.last_valid_targets_metadata_version_number: From ca998ad24a3d99cb3dafb4419a97db41f0d9e3c1 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 15:27:23 -0500 Subject: [PATCH 19/43] Support finding right sig from multiple sigs in p-v secondary code instead of assuming that the first signature is the right one to use. There are reasons that the Director's metadata might have more signatures than a partial verification client may expect. The client need only find the one it needs. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 41 +++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 998778d..6520e25 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -689,13 +689,8 @@ def process_partial_metadata(self, director_targets_metadata_fname): metadata_file_object = tuf.util.load_file(director_targets_metadata_fname) data = metadata_file_object['signed'] - signature = metadata_file_object['signatures'][0] - if director_targets_metadata.endswith('json'): - data = tuf.formats.encode_canonical(data).encode('utf-8') - elif director_targets_metadata.endswith('der'): - data = tuf_asn1_codec.convert_signed_metadata_to_der( # Check to see if the metadata is expired. last_timeserver_time = tuf.formats.datetime_to_unix_timestamp( self.all_valid_timeserver_times[-1]) @@ -714,17 +709,45 @@ def process_partial_metadata(self, director_targets_metadata_fname): 'number than the metadata this partial verification Secondary has ' 'previously validated.') + + # Make sure the data is in the exact format that it is expected to have + # been signed over in order to validate the signature over it. + if director_targets_metadata_fname.endswith('json'): + signed_data = tuf.formats.encode_canonical(data).encode('utf-8') + + elif director_targets_metadata_fname.endswith('der'): + signed_data = tuf_asn1_codec.convert_signed_metadata_to_der( {'signed': data, 'signatures': []}, only_signed=True) - data = hashlib.sha256(data).digest() + signed_data = hashlib.sha256(data).digest() else: # pragma: no cover raise uptane.Error('Unsupported metadata format: ' + repr(metadata_format) + '; the supported formats are: "der" and "json".') - valid = tuf.keys.verify_signature(self.director_public_key, - signature, data) + signatures = metadata_file_object['signatures'] - if not valid: + # Look for a valid signature over the metadata from the expected key. The + # Director's Targets metadata may be signed by a variety of keys that the + # partial verification secondary client isn't concerned with. For example, + # there are edge cases in which after a compromise it may be necessary to + # sign with old and new keys, both, to clear both full verification and + # partial verification checks and reach a partial verification Secondary.) + found_valid_signature = False + for signature in signatures: + + # Don't waste time checking the signature if the keyid (fingerprint) + # or key type (ed25519, rsa, etc.) don't match. + if self.director_public_key['keyid'] != signature['keyid']: + continue + elif self.director_public_key['keytype'] != signature['method']: + continue + + elif tuf.keys.verify_signature( + self.director_public_key, signature, signed_data): + found_valid_signature = True + break + + if not found_valid_signature: log.info( 'Validation failed on an director targets: signature is not valid. ' 'It must be correctly signed by the expected key for that ECU.') From 5dc56ee6d00b9853ca3e8618ec1f6564982b0312 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Tue, 30 Jan 2018 16:11:55 -0500 Subject: [PATCH 20/43] Clarify code and comments and fix bugs from the last few commits Deal correctly with the expiration time format (importing iso8601 to convert it). Rename signed_data to data_signed with modest hope that it will be somewhat less confusing given TUF's nomenclature. Expand on explanations about data conversion in code comments. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 6520e25..603203c 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -40,6 +40,7 @@ import random # for nonces import zipfile # to expand the metadata archive retrieved from the Primary import hashlib +import iso8601 # to manipulate TUF's datetime format for expirations import tuf.formats import tuf.keys @@ -691,12 +692,16 @@ def process_partial_metadata(self, director_targets_metadata_fname): data = metadata_file_object['signed'] - # Check to see if the metadata is expired. - last_timeserver_time = tuf.formats.datetime_to_unix_timestamp( - self.all_valid_timeserver_times[-1]) - expiration_time = tuf.formats.datetime_to_unix_timestamp(data['expires']) + # Check to see if the metadata is expired by comparing it against the + # last validated time provided by the timeserver. (This may be old, and + # we take it as a minimum time.) + minimum_time = tuf.formats.datetime_to_unix_timestamp(iso8601.parse_date( + self.all_valid_timeserver_times[-1])) - if expiration_time < last_timeserver_time: + expiration_time = tuf.formats.datetime_to_unix_timestamp(iso8601.parse_date( + data['expires'])) + + if expiration_time < minimum_time: raise tuf.ExpiredMetadataError('Expired metadata provided to partial ' 'verification Secondary; last valid timeserver time: ' + self.all_valid_timeserver_times[-1] + '; expiration date on ' @@ -712,13 +717,17 @@ def process_partial_metadata(self, director_targets_metadata_fname): # Make sure the data is in the exact format that it is expected to have # been signed over in order to validate the signature over it. + # - In the case of JSON, that means TUF's canonical JSON encoding. + # - In the case of ASN.1/DER, that means a hash taken over the ASN.1/DER + # data (so it must be converted back, as the data was converted into a + # dictionary for ease of use). if director_targets_metadata_fname.endswith('json'): - signed_data = tuf.formats.encode_canonical(data).encode('utf-8') + data_signed = tuf.formats.encode_canonical(data).encode('utf-8') elif director_targets_metadata_fname.endswith('der'): - signed_data = tuf_asn1_codec.convert_signed_metadata_to_der( + data_signed = tuf_asn1_codec.convert_signed_metadata_to_der( {'signed': data, 'signatures': []}, only_signed=True) - signed_data = hashlib.sha256(data).digest() + data_signed = hashlib.sha256(data_signed).digest() else: # pragma: no cover raise uptane.Error('Unsupported metadata format: ' + repr(metadata_format) + @@ -743,7 +752,7 @@ def process_partial_metadata(self, director_targets_metadata_fname): continue elif tuf.keys.verify_signature( - self.director_public_key, signature, signed_data): + self.director_public_key, signature, data_signed): found_valid_signature = True break From c55c0e9a6be2c2d9400bc177abe8cdccc41edf03 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 7 Feb 2018 17:47:36 -0500 Subject: [PATCH 21/43] Preserve last Director-validated firmware info until replaced Secondary.validated_targets_for_this_ecu will no longer be reset before new targets have been validated to replace the old values. Previously, if new metadata was encountered, but no target info was provided for this ECU, valiated_targets_for_this_ecu would be emptied. Instead, for both partial and full verification Secondaries, the most recent target info (hash, length, etc) for firmware a Director instructed this particular ECU to install is retained until it is replaced by new target info that is successfully validated. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 603203c..1a3d856 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -529,8 +529,8 @@ def fully_validate_metadata(self): ENDCOLORS) continue - - self.validated_targets_for_this_ecu = validated_targets_for_this_ecu + if validated_targets_for_this_ecu: + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu @@ -778,7 +778,8 @@ def process_partial_metadata(self, director_targets_metadata_fname): target_metadata['fileinfo'] = targets[target] validated_targets_for_this_ecu.append(target_metadata) - self.validated_targets_for_this_ecu = validated_targets_for_this_ecu + if validated_targets_for_this_ecu: + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu From 05c7328fca779023fba744fc49c983027c3db36e Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 8 Feb 2018 15:15:26 -0500 Subject: [PATCH 22/43] DOC: Move README partial-verification instructions to new section The demo instructions are already pretty busy, so I'm trying to avoid making them more complicated. This way, they're in a separate section. I also add some detail to the explanation of steps that a demo partial-verification Secondary's update_cycle() call performs. Signed-off-by: Sebastien Awwad --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0aa04f3..8b73c40 100644 --- a/README.md +++ b/README.md @@ -177,38 +177,29 @@ Example setting up a different Primary for a different vehicle: Primary have finished starting up and are hosting/listening.) Here, we start a single Secondary ECU and generate a signed ECU Manifest with information about the "firmware" that it is running, which we send to the -Primary. +Primary. Note that these instructions will demonstrate a full-verification +Secondary; for partial-verification Secondaries, see +[this section below](#partial-verification-secondaries). Open a Python shell in a new terminal window and then run the following: -- For full verification secondaries ```python >>> import demo.demo_secondary as ds >>> ds.clean_slate() >>> ds.update_cycle() ``` -- For partial verification secondaries -```python ->>> import demo.demo_secondary as ds ->>> ds.clean_slate(partial_verifying=True) ->>> ds.update_cycle() -``` -Optionally, multiple windows with different Secondary clients can be run simultaneously. In each additional window, you can run the same calls as above to set up a new ECU in the same, default vehicle by modifying the clean_slate() call to include a distinct ECU Serial. e.g. `ds.clean_slate(ecu_serial='33333')` for Full Verification Secondaries or `ds.clean_slate(partial_verifying=True, ecu_serial='33333')` for Partial Verifying Secondaries. +Optionally, multiple windows with different Secondary clients can be run simultaneously. In each additional window, you can run the same calls as above to set up a new ECU in the same, default vehicle by modifying the clean_slate() call to include a distinct ECU Serial. e.g. `ds.clean_slate(ecu_serial='33333')` If the Secondary is in a different vehicle from the default vehicle, this call should look like: `ds.clean_slate(vin='vehicle_id_here', ecu_serial='ecu_serial_here', primary_port=30702)`, providing a VIN for the new vehicle, a unique ECU Serial, and indicating the port listed by this Secondary's Primary when that Primary initialized (e.g. "Primary will now listen on port 30702"). -The Secondary's update_cycle() call for Full Verification Secondaries: +The Secondary's update_cycle() call: - fetches and validates the signed metadata for the vehicle from the Primary - fetches any image that the Primary assigns to this ECU, validating that against the instructions of the Director in the Director's metadata, and against file info available in the Image Repository's metadata. If the image from the Primary does not match validated metadata, it is discarded. - fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver and the signature checks out, this time is saved as valid and reasonably recent. - generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) - submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) -The Secondary's update_cycle() call for Partial Verification Secondaries: -- fetches and validates the signed director's targets metadata for the vehicle from the Primary -- fetches any image that the Primary assigns to this ECU, validating that against the instructions of the Director in the Director's metadata. If the image from the Primary does not match validated metadata, it is discarded. -- fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver and the signature checks out, this time is saved as valid and reasonably recent. - generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) - submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) @@ -622,6 +613,56 @@ have been saved by the Primary. ``` +# Partial-Verification Secondaries +The Secondaries described and employed above ran full verification, employing +the full suite of TUF and Uptane security checks. Uptane provides for a +less demanding alternative: partial-verification Secondaries. These perform +a few checks instead of the full set, at a measured cost to security, allowing +weaker ECUs to still operate as Uptane-conformant Secondaries. + +The distinction is defined +[in Section 8.3 of the Implementation Specification](https://docs.google.com/document/d/1wjg3hl0iDLNh7jIRaHl3IXhwm0ssOtDje5NemyTBcaw/edit?pli=1#heading=h.22u629s8u37q). +Briefly, partial verification entails one signature check to validate metadata, +and one signature check whenever the minimum time is ratcheted forward (for +metadata expiration purposes). Partial-verification Secondaries need to know +the Director's Targets role public key and the Timeserver's public key. + +You can run a demo partial-verification Secondary like so: +(Mind that you have [the Uptane services](#window-1-the-uptane-services) and +[a Primary client](#window-2-the-primary-clients) both running). + +```python +>>> import demo.demo_secondary as ds +>>> ds.clean_slate(partial_verifying=True) +>>> ds.update_cycle() +``` + +The same optional arguments apply for the demo partial-verification Secondary +as for the full-verification version [above](#window-3-the-secondary-clients) +(ECU id/serial, vehicle id/VIN, Primary port, etc.). + + +The Secondary's update_cycle() call does the following: +- fetches the signed Director's Targets role metadata for the vehicle from the +Primary +- checks for a valid signature over the Targets role metadata, from the key it +knows to be the Director's Targets key. +- looks for target info (hash, length, etc.) in the now-validated Targets +metadata that includes this Secondary's ECU identifier, indicating an +instruction from the Director to install that target firmware +- fetches the image the Primary assigns to this ECU +- validates that image against the target info from the validated Targets +metadata. If the image from the Primary does not match validated metadata, it is discarded. +- fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver, the signature is correct and by the expected key, and the time +is more recent than the last validated time, this time is saved as valid, +allowing this Secondary to disregard any metadata with an expiration date +earlier than that time. +- generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be run directly: `ds.generate_signed_ecu_manifest()`) +- submits the ECU Version Manifest to the Primary (can also be run directly: `ds.submit_ecu_manifest_to_primary()`) + + + + # Testing From 70aea1e784740cfa907d68d635d5751b6e54730e Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 8 Feb 2018 15:17:34 -0500 Subject: [PATCH 23/43] DOC: Expand explanation in README of demo clients' update_cycle() Signed-off-by: Sebastien Awwad --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8b73c40..e2e1b53 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,15 @@ Open a Python shell in a new terminal window and then run the following: The Primary's update_cycle() call: - fetches and validates all signed metadata for the vehicle, from the Director and Image repositories -- fetches all images that the Director instructs this vehicle to install, excluding any that do not exactly match corresponding images on the Image repository. Any images fetched from the repositories that do not match validated metadata are discarded. +- reads the Director's Targets metadata to determine what targets the Director +has instructed this vehicle to install, storing the trustworthy target info for +each (hash, length, etc.), and comparing it to similar info from the Image +repository. A mismatch results in that installation instruction being ignored. +- fetches the target images indicated and compares them to the trusted info +(hash, length, etc.). Any images fetched +from the repositories that do not match validated metadata are discarded. - queries the Timeserver for a signed attestation about the current time, including in it any nonces sent by Secondaries, so that Secondaries may trust that the time returned is at least as recent as their sent nonce +- validates the Timeserver's time attestations for itself and updates its time - generates a Vehicle Version Manifest with some vehicle metadata and all ECU Version Manifests received from Secondaries, describing currently installed images, most recent times available to each ECU, and reports of any attacks observed by Secondaries (can also be called directly: `dp.generate_signed_vehicle_manifest()`) - sends that Vehicle Version Manifest to the Director (can also be called directly: `dp.submit_vehicle_manifest_to_director()`) @@ -195,11 +202,17 @@ If the Secondary is in a different vehicle from the default vehicle, this call s The Secondary's update_cycle() call: - fetches and validates the signed metadata for the vehicle from the Primary -- fetches any image that the Primary assigns to this ECU, validating that against the instructions of the Director in the Director's metadata, and against file info available in the Image Repository's metadata. If the image from the Primary does not match validated metadata, it is discarded. -- fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation from the Timeserver and the signature checks out, this time is saved as valid and reasonably recent. -- generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) -- submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) - +- looks for target info (hash, length, etc.) in the now-validated Targets +metadata that includes this Secondary's ECU identifier, indicating an +instruction from the Director to install that target firmware +- fetches any image that the Primary assigns to this ECU, validating that +against the target info from the validated Director metadata. If the image from +the Primary does not match validated metadata, it is discarded. +- fetches the latest Timeserver attestation from the Primary, checking for the nonce this Secondary last sent. If that nonce is included in the signed attestation +from the Timeserver, the signature is correct and by the expected key, and the +time is more recent than the last validated time, this time is saved as valid, +allowing this Secondary to disregard any metadata with an expiration date +earlier than that time. - generates an ECU Version Manifest that indicates the secure hash of the image currently installed on this Secondary, the latest validated times, and a string describing attacks detected (can also be called directly: `ds.generate_signed_ecu_manifest()`) - submits the ECU Version Manifest to the Primary (can also be called directly: `ds.submit_ecu_manifest_to_primary()`) From 51ed0cbaa6c05e386d873b213e0ed22cc5f356f2 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 8 Feb 2018 16:32:23 -0500 Subject: [PATCH 24/43] Make partial-verification Secondary respect tuf.conf.METADATA_FORMAT Previously, the partial verification Secondary code (same PR) would determine whether to analyze Director Targets metadata provided to it as JSON or DER based on the file extension. This is not consistent with the rest of the code in the reference implementation, so it is corrected here. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 1a3d856..f7ea806 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -721,10 +721,10 @@ def process_partial_metadata(self, director_targets_metadata_fname): # - In the case of ASN.1/DER, that means a hash taken over the ASN.1/DER # data (so it must be converted back, as the data was converted into a # dictionary for ease of use). - if director_targets_metadata_fname.endswith('json'): + if tuf.conf.METADATA_FORMAT == 'json': data_signed = tuf.formats.encode_canonical(data).encode('utf-8') - elif director_targets_metadata_fname.endswith('der'): + elif tuf.conf.METADATA_FORMAT == 'der': data_signed = tuf_asn1_codec.convert_signed_metadata_to_der( {'signed': data, 'signatures': []}, only_signed=True) data_signed = hashlib.sha256(data_signed).digest() From 98ae7cb352b3d28b4070c7e70ed036e8c7680cc8 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 8 Feb 2018 16:39:00 -0500 Subject: [PATCH 25/43] Fix p-v Secondary test code given correction to format determination Since the partial-verification Secondary now respects tuf.conf.METADATA_FORMAT, the test code can now stop trying to test both formats in both testing modes and instead just test based on content and let tuf.conf.METADATA_FORMAT determine which mode to work in. Added sample bad sig Director Targets metadata in DER format for use in the test. Signed-off-by: Sebastien Awwad --- ...e_pv_secondary_target_metadata_bad_sig.der | Bin 0 -> 299 bytes tests/test_secondary.py | 56 ++++++------------ 2 files changed, 19 insertions(+), 37 deletions(-) create mode 100644 samples/sample_pv_secondary_target_metadata_bad_sig.der diff --git a/samples/sample_pv_secondary_target_metadata_bad_sig.der b/samples/sample_pv_secondary_target_metadata_bad_sig.der new file mode 100644 index 0000000000000000000000000000000000000000..db7f61fa6ff8fef6e7abe4fe8b1528387eb15436 GIT binary patch literal 299 zcmV+`0o495f&nL>fwF)B0f7Wx`x(uG0Rp3eqoIMIfB^xaftoOZm!N@*fDbQbX>x6M zVRB_LGcIXuXMq6}f&l`faxf==0Rf>RfgsxM!c|q7xaX)VZxcDy^cgg3SD@>9IqqXL zxbg*AS5h!XfB^%cLV-XPaFcq3>E}*0_2p`hg&4^m7$uDV$gJW?*{tz%(S%)s040O5 zwKH?Kp5IbQO;S%L8TIjrZ!m=ejqIr`l>~;N2Z9AMFfcGMfdK)cZ7^$~B7q=d4E2Qb zl0yq28KgO0Ibu$R_2tDkMF*#ezFhNL@Q~Jl0Rf^yfk35TF)Qfn)KSKXa!NkJ{Jpoj xaEm8qm|@@)Yt|Yn74S>1knP7_-7Z6V#(<^Lv)5}rOP6M-sbLU`_O#^zl@4Z1g_r;U literal 0 HcmV?d00001 diff --git a/tests/test_secondary.py b/tests/test_secondary.py index d5d1afa..be6c954 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -738,12 +738,10 @@ def test_45_process_partial_metadata(self): Tests uptane.clients.secondary.Secondary::process_partial_metadata() Tests PV Secondary client in 2 situations: - - secondary_instances[3]: Director's targets metadata available with - valid signatures in JSON mode - - secondary_instances[3]: Director's targets metadata available with - invalid signatures in JSON mode - - secondary_instances[3]: Director's targets metadata available with - valid signatures in DER mode + - secondary_instances[3]: Director's targets metadata available with + valid signatures + - secondary_instances[3]: Director's targets metadata available with + invalid signatures """ # --- Test this test module's setup (defensive) # First, check the source directories, from which the temp dir is copied. @@ -756,46 +754,30 @@ def test_45_process_partial_metadata(self): self.assertEqual( ['root.der', 'root.json'], sorted(os.listdir(data_directory))) - - sample_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', - 'sample_pv_secondary_target_metadata.json') - sample_bad_sig_working_metadata_path = os.path.join(uptane.WORKING_DIR, - 'samples', 'sample_pv_secondary_target_metadata_bad_sig.json') + sample_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', + 'sample_pv_secondary_target_metadata.' + tuf.conf.METADATA_FORMAT) - sample_der_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', - 'sample_pv_secondary_target_metadata.der') + sample_bad_sig_working_metadata_path = os.path.join(uptane.WORKING_DIR, + 'samples','sample_pv_secondary_target_metadata_bad_sig.' + + tuf.conf.METADATA_FORMAT) pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] instance = secondary_instances[PV_SECONDARY1_INDICE] - director_targets_metadata_path_json = os.path.join(pv_secondary_dir, - 'metadata', 'director_targets.json') + director_targets_metadata_path = os.path.join(pv_secondary_dir, + 'metadata', 'director_targets.' + tuf.conf.METADATA_FORMAT) - director_targets_metadata_path_der = os.path.join(pv_secondary_dir, - 'metadata', 'director_targets.der') - - # PV Secondary 1 with valid director public key and update JSON - shutil.copy(sample_working_metadata_path, director_targets_metadata_path_json) - if not os.path.exists(director_targets_metadata_path_json): - raise("Director's targets not available") - instance.process_metadata(director_targets_metadata_path_json) + # PV Secondary 1 with valid director public key. Update successfully. + shutil.copy(sample_working_metadata_path, director_targets_metadata_path) # <~> Is this right? + instance.process_metadata(director_targets_metadata_path) # PV Secondary 1 with valid director public key but update with - # invalid signature. JSON - shutil.copy(sample_bad_sig_working_metadata_path, - director_targets_metadata_path_json) - if not os.path.exists(director_targets_metadata_path_json): - raise("Director's targets not available") + # invalid signature + shutil.copy(sample_bad_sig_working_metadata_path, + director_targets_metadata_path) with self.assertRaises(tuf.BadSignatureError): - instance.process_metadata(director_targets_metadata_path_json) - - # PV Secondary 1 with valid director public key and update DER - shutil.copy(sample_der_working_metadata_path, - director_targets_metadata_path_der) - if not os.path.exists(director_targets_metadata_path_der): - raise("Director's targets not available") - instance.process_metadata(director_targets_metadata_path_der) - + instance.process_metadata(director_targets_metadata_path) + From 593e48e1338e392681463e96e249d4ab29179a24 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 8 Feb 2018 16:44:39 -0500 Subject: [PATCH 26/43] Add test to p-v Secondary tests: missing Director pub key Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index be6c954..6e7f5fd 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -778,6 +778,16 @@ def test_45_process_partial_metadata(self): with self.assertRaises(tuf.BadSignatureError): instance.process_metadata(director_targets_metadata_path) + # If the Secondary lacks a Director public key for some reason (even + # though the constructor checks for one if this is a partial-verification + # Secondary), it should raise this error: + with self.assertRaises(uptane.Error): + temp = instance.director_public_key + instance.director_public_key = None + instance.process_metadata(director_targets_metadata_path) + + instance.director_public_key = temp # put the key back after the test + From 9f073412355e020adcc49a5f6f621f1a4b81f386 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 12:54:03 -0500 Subject: [PATCH 27/43] DOC: Note state of test and sample files for secondary in a comment in test_secondary Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 6e7f5fd..714f731 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -37,6 +37,23 @@ # For temporary convenience: import demo # for generate_key, import_public_key, import_private_key +# TODO: Test data directories are somewhat more convoluted than necessary. +# The tests/test_data/ directory (TEST_DATA_DIR) contains: +# - director_metadata and image_repo_metadata directories, which each contain +# only root.json and root.der, sane files for use in testing. +# - flawed_manifests (with correct and various flawed vehicle and ECU +# manifests) +# - pinned.json, a sane pinning file for use in testing +# - temporary directories created during testing: +# - temp_test_secondary0, temp_test_partial_secondary0, etc. +# - temp_test_common, which seems to be unused and persists...? +# +# The samples/ directory (SAMPLE_DATA_DIR) contains snapshots of all repository +# metadata files from both repositories in a few states, with distant +# expiration dates (decades). It also contains a variety of samples of +# manifests and time attestations, along with flawed samples (expired, bad +# signatures, etc.) for both human consumption and testing purposes. +# TEST_DATA_DIR = os.path.join(uptane.WORKING_DIR, 'tests', 'test_data') TEST_DIRECTOR_METADATA_DIR = os.path.join(TEST_DATA_DIR, 'director_metadata') TEST_IMAGE_REPO_METADATA_DIR = os.path.join( From ce97644e614cc4553b4c8cbd92cb50452343f0df Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 12:56:53 -0500 Subject: [PATCH 28/43] Simplify some pv secondary test variables in test_secondary Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 714f731..74f10d6 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -63,6 +63,7 @@ TEST_IMAGE_REPO_ROOT_FNAME = os.path.join( TEST_IMAGE_REPO_METADATA_DIR, 'root.' + tuf.conf.METADATA_FORMAT) TEST_PINNING_FNAME = os.path.join(TEST_DATA_DIR, 'pinned.json') +SAMPLE_DATA_DIR = os.path.join(uptane.WORKING_DIR, 'samples') TEMP_CLIENT_DIRS = [ os.path.join(TEST_DATA_DIR, 'temp_test_secondary0'), @@ -655,9 +656,9 @@ def test_40_process_metadata(self): # --- Set up this test # Location of the sample Primary-produced metadata archive - sample_archive_fname = os.path.join( - uptane.WORKING_DIR, 'samples', 'metadata_samples_long_expiry', - 'update_to_one_ecu', 'full_metadata_archive.zip') + sample_archive_fname = os.path.join(SAMPLE_DATA_DIR, + 'metadata_samples_long_expiry', 'update_to_one_ecu', + 'full_metadata_archive.zip') assert os.path.exists(sample_archive_fname), 'Cannot test ' \ 'process_metadata; unable to find expected sample metadata archive' + \ @@ -772,11 +773,11 @@ def test_45_process_partial_metadata(self): ['root.der', 'root.json'], sorted(os.listdir(data_directory))) - sample_working_metadata_path = os.path.join(uptane.WORKING_DIR, 'samples', + working_metadata_path = os.path.join(SAMPLE_DATA_DIR, 'sample_pv_secondary_target_metadata.' + tuf.conf.METADATA_FORMAT) - sample_bad_sig_working_metadata_path = os.path.join(uptane.WORKING_DIR, - 'samples','sample_pv_secondary_target_metadata_bad_sig.' + + bad_sig_metadata_path = os.path.join(SAMPLE_DATA_DIR, + 'sample_pv_secondary_target_metadata_bad_sig.' + tuf.conf.METADATA_FORMAT) pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] @@ -785,13 +786,13 @@ def test_45_process_partial_metadata(self): 'metadata', 'director_targets.' + tuf.conf.METADATA_FORMAT) # PV Secondary 1 with valid director public key. Update successfully. - shutil.copy(sample_working_metadata_path, director_targets_metadata_path) # <~> Is this right? + # The metadata happens to have version == 2 (relevant in the next tests). + shutil.copy(working_metadata_path, director_targets_metadata_path) # <~> Is this right? instance.process_metadata(director_targets_metadata_path) # PV Secondary 1 with valid director public key but update with # invalid signature - shutil.copy(sample_bad_sig_working_metadata_path, - director_targets_metadata_path) + shutil.copy(bad_sig_metadata_path, director_targets_metadata_path) with self.assertRaises(tuf.BadSignatureError): instance.process_metadata(director_targets_metadata_path) From 9539c8c6643173768ab29218c3c002f99a35dbc0 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 12:58:25 -0500 Subject: [PATCH 29/43] Correct use of tuf.ReplayedMetadataError (same PR) tuf.ReplayedMetadataError expects specific arguments rather than just taking an error string - the metadata type, previously validated version, and currently received (replayed) version. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index f7ea806..7c5dbbd 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -710,10 +710,10 @@ def process_partial_metadata(self, director_targets_metadata_fname): # Check to see if the metadata has a lower version number than expected. if data['version'] < self.last_valid_targets_metadata_version_number: - raise tuf.ReplayedMetadataError('Provided metadata has lower version ' - 'number than the metadata this partial verification Secondary has ' - 'previously validated.') - + log.error('Provided metadata has lower version number than the metadata ' + 'this partial verification Secondary has previously validated.') + raise tuf.ReplayedMetadataError('targets', + self.last_valid_targets_metadata_version_number, data['version']) # Make sure the data is in the exact format that it is expected to have # been signed over in order to validate the signature over it. From e26b12f6193602c21ff832f0608d2c219139dcbd Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 13:02:08 -0500 Subject: [PATCH 30/43] Test partial verification Secondary for expired and replayed metadata Includes the addition of several sample metadata files for testing. These are now used in test_secondary. Signed-off-by: Sebastien Awwad --- samples/director_targets_empty_v1.der | Bin 0 -> 140 bytes samples/director_targets_empty_v1.json | 19 +++++++++++++++++++ samples/director_targets_expired_v3.der | Bin 0 -> 140 bytes samples/director_targets_expired_v3.json | 19 +++++++++++++++++++ tests/test_secondary.py | 21 ++++++++++++++++++++- 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 samples/director_targets_empty_v1.der create mode 100644 samples/director_targets_empty_v1.json create mode 100644 samples/director_targets_expired_v3.der create mode 100644 samples/director_targets_expired_v3.json diff --git a/samples/director_targets_empty_v1.der b/samples/director_targets_empty_v1.der new file mode 100644 index 0000000000000000000000000000000000000000..99c8f2e3c03a849f5c321303e252359367066a96 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41av@A=YjzNqX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-YVE_rODKLn9Jq^J}NwGw-$$EQf0 uVbjX(RD4@7rinh#p-Fd42YQZe@{Ep*->2)dQpoA67gIV&up-TiUTRp!3 literal 0 HcmV?d00001 diff --git a/samples/director_targets_empty_v1.json b/samples/director_targets_empty_v1.json new file mode 100644 index 0000000..d1d0018 --- /dev/null +++ b/samples/director_targets_empty_v1.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "f474702d61728fbfa01cc6d9b6e9b6681710680927efc2cdcbae1e758b89ddb355c8ce2c636ab26160040c9a575265b81311a4e40349463f34e784b48d4e850e" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2031-10-21T18:00:34Z", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/director_targets_expired_v3.der b/samples/director_targets_expired_v3.der new file mode 100644 index 0000000000000000000000000000000000000000..6f35e23e7340263a0ae0f7800b26a0bdb3701e36 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41X_))wt@izqX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-X8DD47l1B0gOn#{WD$ZbQQN@)S% uwTxm|dH4=9$KD{$BkGHK97=U2DCOj&Q5!gT&$cU0(lX Date: Wed, 21 Feb 2018 13:03:30 -0500 Subject: [PATCH 31/43] DOC: Add clarifying comment to temp vars in test_secondary and trim some trailing whitespace in test_secondary (from this same PR) Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index aa24df7..8358869 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -71,7 +71,8 @@ os.path.join(TEST_DATA_DIR, 'temp_test_secondary2'), os.path.join(TEST_DATA_DIR, 'temp_test_partial_secondary0')] -# Assigns the last NUM_PARTIAL_SECONDARIES number of + +# Assigns the last NUM_PARTIAL_SECONDARIES number of # secondaries as partial verification NUM_PARTIAL_SECONDARIES = 1 # Indices of PV Secondaries in the list of secondaries @@ -79,7 +80,7 @@ # I'll initialize these in the __init__ test, and use this for the simple # non-damaging tests so as to avoid creating objects all over again. -# Initializes the number of secondary instances to the number of +# Initializes the number of secondary instances to the number of # TEMP_CLIENT_DIRS secondary_instances = [None] * len(TEMP_CLIENT_DIRS) @@ -389,7 +390,7 @@ def test_01_init(self): vin = vins[i] partial_verifying_for_ecu = False director_public_key_for_ecu = None - # Sets the last NUM_PARTIAL_SECONDARIES number of ECUs to partial + # Sets the last NUM_PARTIAL_SECONDARIES number of ECUs to partial # verifying if i == PV_SECONDARY1_INDICE: partial_verifying_for_ecu = True @@ -786,6 +787,9 @@ def test_45_process_partial_metadata(self): replayed_metadata_path = os.path.join(SAMPLE_DATA_DIR, 'director_targets_empty_v1.' + tuf.conf.METADATA_FORMAT) + # director_targets_metadata_path is where the partial verification Secondary + # client stores the Director Targets metadata it gets from the Primary, + # which it then will validate. pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] instance = secondary_instances[PV_SECONDARY1_INDICE] director_targets_metadata_path = os.path.join(pv_secondary_dir, From 282c0ba7f0091b4370b91c85f0d30f0029afdfbf Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 16:12:57 -0500 Subject: [PATCH 32/43] Error if Secondary receives time attest. before ever sending nonces Previous behavior was to return from the time attestation validation function without updating the time or raising an error. Now, the behavior is to raise an error, which is consistent with the behavior if the Secondary's nonce is missing from the received time attestation. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 7c5dbbd..2311071 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -438,10 +438,12 @@ def validate_time_attestation(self, timeserver_attestation): if self.last_nonce_sent is None: # This ECU is fresh and hasn't actually ever sent a nonce to the Primary # yet. It would be impossible to validate a timeserver attestation. - log.warning(YELLOW + 'Cannot validate a timeserver attestation yet: ' + log.error('Cannot validate a timeserver attestation yet: ' 'this fresh Secondary ECU has never communicated a nonce and ECU ' - 'Version Manifest to the Primary.' + ENDCOLORS) - return + 'Version Manifest to the Primary.') + raise uptane.BadTimeAttestation('This Secondary has been have been ' + 'provided a time attestation, but there is no record of this ' + 'Secondary having ever previously sent any nonce.') elif self.last_nonce_sent not in timeserver_attestation['signed']['nonces']: # TODO: Create a new class for this Exception in this file. From c7299b7e878d5d362976b963a616fa22c4d5226b Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 16:18:04 -0500 Subject: [PATCH 33/43] Test time attestation validation by Secondary that never sent a nonce Expect error similar to time attestation validation when the nonce the Secondary expects is not included. Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 8358869..a6367cd 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -574,6 +574,15 @@ def test_20_validate_time_attestation(self): instance.validate_time_attestation(time_attestation__wrongnonce) + # Conduct one test with a different secondary instance: + # Expect that if a time attestation is submitted to be validated by a + # Secondary that hasn't ever sent a nonce, the validation function will + # reject the time attestation. (Because it doesn't matter, we'll use the + # same sensible time attestation previously generated in this test func.) + instance = secondary_instances[1] + with self.assertRaises(uptane.BadTimeAttestation): + instance.validate_time_attestation(time_attestation) + # TODO: Consider other tests here. From 9b83a38c389d661be95aa1e2b8c178a0e7d3f698 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 16:19:18 -0500 Subject: [PATCH 34/43] Additional testing of p-v Secondary signature checking Test result if signature is by key ID that is not expected (but sig is valid) and if signature is by key ID that is expected but the key type is not the type expected. (Increases code coverage) Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index a6367cd..880a696 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -809,6 +809,31 @@ def test_45_process_partial_metadata(self): shutil.copy(working_metadata_path, director_targets_metadata_path) # <~> Is this right? instance.process_metadata(director_targets_metadata_path) + # If the Secondary expects a signature from a key of a different type than + # the one that signed the metadata, expect failure (whether or not it + # has the same key ID). + assert instance.director_public_key['keytype'] == 'ed25519', 'This test ' \ + 'is no longer correct: it assumes that the key type of the Director ' \ + 'Targets key will be ed25519, but it is actually ' + \ + instance.director_public_key['keytype'] + '; please fix the test.' + instance.director_public_key['keytype'] = 'rsa' + with self.assertRaises(tuf.BadSignatureError): + instance.process_metadata(director_targets_metadata_path) + instance.director_public_key['keytype'] = 'ed25519' # back to real key type + + # If the Secondary expects a signature from a different key than the one + # that signed the metadata, expect failure. + temp = instance.director_public_key + instance.director_public_key = self.key_timeserver_pub + with self.assertRaises(tuf.BadSignatureError): + instance.process_metadata(director_targets_metadata_path) + instance.director_public_key = temp # put the key back after the test + + # TODO: Make sure that it doesn't interfere with validation if there are + # other, unnecessary signatures on the metadata before the signature that + # the partial verification Secondary is expecting. + + # PV Secondary 1 with valid director public key but update with # invalid signature. version == 2 shutil.copy(bad_sig_metadata_path, director_targets_metadata_path) From 700b4f03159fa2c100a0548300eef17dfe23a4aa Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 17:09:31 -0500 Subject: [PATCH 35/43] Test p-v Secondary behavior if Director Targets file is missing Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 880a696..5f58e49 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -804,6 +804,10 @@ def test_45_process_partial_metadata(self): director_targets_metadata_path = os.path.join(pv_secondary_dir, 'metadata', 'director_targets.' + tuf.conf.METADATA_FORMAT) + # First, test behavior if the file we indicate does not exist. + with self.assertRaises(uptane.Error): + instance.process_metadata('some_file_that_does_not_actually_exist.xyz') + # PV Secondary 1 with valid director public key. Update successfully. # The metadata happens to have version == 2 (relevant in the next tests). shutil.copy(working_metadata_path, director_targets_metadata_path) # <~> Is this right? From 85351184ea94bed76fb9c0eb77773cef4bcec74a Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 17:27:04 -0500 Subject: [PATCH 36/43] DOC: Tidy up some demo comments in demo_secondary Signed-off-by: Sebastien Awwad --- demo/demo_secondary.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index 4c7bbbf..e2bd640 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -307,14 +307,15 @@ def update_cycle(): # from it like so: time_attestation = time_attestation.data - # If the secondary runs in full verification mode: - # The metadata is downloaded from the Primary in the form of an archive - # that includes all the metadata files. This returns the binary data that - # we need to write to the file. - # If the secondary runs in partial verification mode: - # Only the director's targets role file is copied. - - metadata_from_primary = pserver.get_metadata(secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) + # Obtain metadata from the Primary, either as a single role file (the + # Director Targets role file) if this is a partial-verification Secondary, + # or as an archive that includes all the metadata files if this is a full- + # verification Secondary. + # This call returns the binary data that we need to write to the file. + + metadata_from_primary = pserver.get_metadata( + secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) + # Validate the time attestation and internalize the time. Continue # regardless. try: @@ -331,10 +332,8 @@ def update_cycle(): #else: # print(GREEN + 'Official time has been updated successfully.' + ENDCOLORS) - # If secondary is running full validation then - # Dump the archive file to disk. - # If secondary is running partial verification then - # copies the director targets role file. + # Write the metadata retrieved from the Primary to disk, whether it is a + # single role file (partial verification) or the full metadata archive. if secondary_ecu.partial_verifying: director_targets_role = os.path.join( secondary_ecu.full_client_dir, 'director_targets.'+tuf.conf.METADATA_FORMAT) @@ -347,9 +346,8 @@ def update_cycle(): with open(archive_fname, 'wb') as fobj: fobj.write(metadata_from_primary.data) - # Now tell the Secondary reference implementation code where the archive file - # is and let it expand and validate the metadata. - + # Now tell the Secondary reference implementation code where the archive + # file is and let it expand (as necessary) and validate the metadata. secondary_ecu.process_metadata(archive_fname) From 97558861d3c6e4741beddb0da65d43ed5b88c609 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Wed, 21 Feb 2018 17:29:59 -0500 Subject: [PATCH 37/43] DOC: Clarify error conditions in process_partial_metadata docstring Also slightly rewords the beginning of an error message to make it clearer. Signed-off-by: Sebastien Awwad --- uptane/clients/secondary.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 2311071..aee8dd6 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -662,9 +662,16 @@ def process_partial_metadata(self, director_targets_metadata_fname): None + uptane.Error + if director_targets_metadata_fname does not specify a file that exists + or if tuf.conf.METADATA_FORMAT is somehow an unsupported format (i.e. + not 'json' or 'der') + tuf.BadSignatureError if the signature over the Targets metadata is not a valid - signature by the key corresponding to self.director_public_key. + signature by the key corresponding to self.director_public_key, or if + the key type listed in the signature does not match the key type listed + in the public key tuf.ExpiredMetadataError if the Targets metadata is expired @@ -686,7 +693,7 @@ def process_partial_metadata(self, director_targets_metadata_fname): validated_targets_for_this_ecu = [] target_metadata = {} if not os.path.exists(director_targets_metadata_fname): - raise uptane.Error('Indicated metadata archive does not exist. ' + raise uptane.Error('Indicated Director Targets metadata file not found. ' 'Filename: ' + repr(director_targets_metadata_fname)) metadata_file_object = tuf.util.load_file(director_targets_metadata_fname) From 69310a8c3dd36284a0b15e70ce40d87ede5be892 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 10:27:58 -0500 Subject: [PATCH 38/43] Rename four sample / test data files used by test_secondary to have shorter names that express what the files are (rather than one example usage). Also updates test_secondary to point to the new names. Signed-off-by: Sebastien Awwad --- ..._bad_sig.der => director_targets_bad_sig_v2.der} | Bin ...ad_sig.json => director_targets_bad_sig_v2.json} | 0 ..._target_metadata.der => director_targets_v2.der} | Bin ...arget_metadata.json => director_targets_v2.json} | 0 tests/test_secondary.py | 5 ++--- 5 files changed, 2 insertions(+), 3 deletions(-) rename samples/{sample_pv_secondary_target_metadata_bad_sig.der => director_targets_bad_sig_v2.der} (100%) rename samples/{sample_pv_secondary_target_metadata_bad_sig.json => director_targets_bad_sig_v2.json} (100%) rename samples/{sample_pv_secondary_target_metadata.der => director_targets_v2.der} (100%) rename samples/{sample_pv_secondary_target_metadata.json => director_targets_v2.json} (100%) diff --git a/samples/sample_pv_secondary_target_metadata_bad_sig.der b/samples/director_targets_bad_sig_v2.der similarity index 100% rename from samples/sample_pv_secondary_target_metadata_bad_sig.der rename to samples/director_targets_bad_sig_v2.der diff --git a/samples/sample_pv_secondary_target_metadata_bad_sig.json b/samples/director_targets_bad_sig_v2.json similarity index 100% rename from samples/sample_pv_secondary_target_metadata_bad_sig.json rename to samples/director_targets_bad_sig_v2.json diff --git a/samples/sample_pv_secondary_target_metadata.der b/samples/director_targets_v2.der similarity index 100% rename from samples/sample_pv_secondary_target_metadata.der rename to samples/director_targets_v2.der diff --git a/samples/sample_pv_secondary_target_metadata.json b/samples/director_targets_v2.json similarity index 100% rename from samples/sample_pv_secondary_target_metadata.json rename to samples/director_targets_v2.json diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 5f58e49..5c398de 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -784,11 +784,10 @@ def test_45_process_partial_metadata(self): sorted(os.listdir(data_directory))) working_metadata_path = os.path.join(SAMPLE_DATA_DIR, - 'sample_pv_secondary_target_metadata.' + tuf.conf.METADATA_FORMAT) + 'director_targets_v2.' + tuf.conf.METADATA_FORMAT) bad_sig_metadata_path = os.path.join(SAMPLE_DATA_DIR, - 'sample_pv_secondary_target_metadata_bad_sig.' + - tuf.conf.METADATA_FORMAT) + 'director_targets_bad_sig_v2.' + tuf.conf.METADATA_FORMAT) expired_metadata_path = os.path.join(SAMPLE_DATA_DIR, 'director_targets_expired_v3.' + tuf.conf.METADATA_FORMAT) From 1c5bd28215a34eac44ad5ab1445494889aa41997 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 10:30:41 -0500 Subject: [PATCH 39/43] DOC: Minor style adjustment and removal of whitespace change Fixes two changes in this PR: uses '.' instead of C++ style '::' class member notation (my bad habit) in test_secondary and removes the single whitespace change to demo_primary made in this PR, for PR cleanliness and code readability. Signed-off-by: Sebastien Awwad --- demo/demo_primary.py | 1 + tests/test_secondary.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/demo_primary.py b/demo/demo_primary.py index 98647a9..1ae9158 100644 --- a/demo/demo_primary.py +++ b/demo/demo_primary.py @@ -488,6 +488,7 @@ def get_metadata_for_ecu(ecu_serial, force_partial_verification=False): """ # Ensure serial is correct format & registered primary_ecu._check_ecu_serial(ecu_serial) + # The filename of the file to return. fname = None diff --git a/tests/test_secondary.py b/tests/test_secondary.py index 5c398de..e0e0a00 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -763,7 +763,7 @@ def test_40_process_metadata(self): def test_45_process_partial_metadata(self): """ - Tests uptane.clients.secondary.Secondary::process_partial_metadata() + Tests uptane.clients.secondary.Secondary.process_partial_metadata() Tests PV Secondary client in 2 situations: - secondary_instances[3]: Director's targets metadata available with From eb7793930842b67bde9a4463981dfc419dd3a489 Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 10:33:52 -0500 Subject: [PATCH 40/43] Add some more sample Director Targets metadata for future use Will spare someone the effort of generating it, for future tests. Signed-off-by: Sebastien Awwad --- samples/director_targets_empty_v2.der | Bin 0 -> 140 bytes samples/director_targets_empty_v2.json | 19 +++++++++++++++++++ samples/director_targets_empty_v3.der | Bin 0 -> 140 bytes samples/director_targets_empty_v3.json | 19 +++++++++++++++++++ samples/director_targets_expired_v1.der | Bin 0 -> 140 bytes samples/director_targets_expired_v1.json | 19 +++++++++++++++++++ samples/director_targets_expired_v2.der | Bin 0 -> 140 bytes samples/director_targets_expired_v2.json | 19 +++++++++++++++++++ 8 files changed, 76 insertions(+) create mode 100644 samples/director_targets_empty_v2.der create mode 100644 samples/director_targets_empty_v2.json create mode 100644 samples/director_targets_empty_v3.der create mode 100644 samples/director_targets_empty_v3.json create mode 100644 samples/director_targets_expired_v1.der create mode 100644 samples/director_targets_expired_v1.json create mode 100644 samples/director_targets_expired_v2.der create mode 100644 samples/director_targets_expired_v2.json diff --git a/samples/director_targets_empty_v2.der b/samples/director_targets_empty_v2.der new file mode 100644 index 0000000000000000000000000000000000000000..a73391386ab8fa88516c71f47677b519b1904754 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41av@A=YjzOqX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-Y5qg#Ur-VrRzRkvVW;fATHaHBj# u+7P(Bv&G^tqfXOTPM_jo_+tXzg0(1&shr(Kpe(@2PY-4m<6jy?4u%e8t2|5q literal 0 HcmV?d00001 diff --git a/samples/director_targets_empty_v2.json b/samples/director_targets_empty_v2.json new file mode 100644 index 0000000..2eba167 --- /dev/null +++ b/samples/director_targets_empty_v2.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "3b6f11eb9382fdbdbbcce54641617d8ea159d065f70484a3b013641ef22619902b252692286f6741e1fd9d68b3805dc727136b0b3ccc29c51b4bc97ecaf2f708" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2031-10-21T18:00:34Z", + "targets": {}, + "version": 2 + } +} \ No newline at end of file diff --git a/samples/director_targets_empty_v3.der b/samples/director_targets_empty_v3.der new file mode 100644 index 0000000000000000000000000000000000000000..4a5d18f114254917922e2c7b0565917f3707dab0 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41av@A=YjzPqX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-YcrNq_|u}sIH@V&+Sj#}H*DE$Wk uD|@J);^v^$RuH~oaMA3NBBi;TpJ?NleVaw9Fc+Z0!^V!67ot$NXJP^bbwHH> literal 0 HcmV?d00001 diff --git a/samples/director_targets_empty_v3.json b/samples/director_targets_empty_v3.json new file mode 100644 index 0000000..4b976d9 --- /dev/null +++ b/samples/director_targets_empty_v3.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "a36a0816ffe796a451d431a4f9b9f5df350650c69c36bf22397631c4418f6778f5da01e302d19e565638616cb0c32dcc3caf3cc0e92741a0a284a8b9cf62480a" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2031-10-21T18:00:34Z", + "targets": {}, + "version": 3 + } +} \ No newline at end of file diff --git a/samples/director_targets_expired_v1.der b/samples/director_targets_expired_v1.der new file mode 100644 index 0000000000000000000000000000000000000000..950695b0b71d4072c233147952a1a47794687747 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41X~6ABZ2_}qX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-X~I??fX4CQrY=#}0EGQhcIWFRn- uWvQFlJ={hpZI;Hxy*xr`#6(fAB2pgoTO literal 0 HcmV?d00001 diff --git a/samples/director_targets_expired_v1.json b/samples/director_targets_expired_v1.json new file mode 100644 index 0000000..eb89162 --- /dev/null +++ b/samples/director_targets_expired_v1.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "fb26d9d8f9314175cc7d6cfdcc0628a57f24f9ef43b84a34496b06c7e53864f6581e02a8589fc307603e9de0275ba0edce8044a010666f7f72ca70c423b4e10d" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2018-02-21T16:14:48Z", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/samples/director_targets_expired_v2.der b/samples/director_targets_expired_v2.der new file mode 100644 index 0000000000000000000000000000000000000000..d70ea9b8524101c8ebcd9be59e38a8613aa7e5d0 GIT binary patch literal 140 zcmV;70CWE^fr+3MfB^x41X_))wt@iyqX(e{fB^uZ0D%DkqHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-Y;fQJxP=Yi{&hXwZ-EK111ylXdB u(>J7vHCd53WM|Hr9%DrU!&Qds+#d}V%~@9Mb_ooV;l{G@eke2jus#e^|2wV# literal 0 HcmV?d00001 diff --git a/samples/director_targets_expired_v2.json b/samples/director_targets_expired_v2.json new file mode 100644 index 0000000..7ff2ada --- /dev/null +++ b/samples/director_targets_expired_v2.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", + "method": "ed25519", + "sig": "2f43ed700ee2205de1ffea87b7008ecb1bac8777895f8565cfd23c3a8b32fd9ac882f5e94b772ff0e9ad10c49f6be14cba65b39c95eeae944fa5831d6de23a0e" + } + ], + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2018-02-21T17:36:11Z", + "targets": {}, + "version": 2 + } +} \ No newline at end of file From 0f845904314816d04ee740a268b11b39f055d34c Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 16:27:35 -0500 Subject: [PATCH 41/43] Reorganize test data use in test_secondary to use a more sensible data structure. The next two commits will address the remainder of the code (test_50_validate_image). Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 236 +++++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 113 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index e0e0a00..bab18e4 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -65,30 +65,40 @@ TEST_PINNING_FNAME = os.path.join(TEST_DATA_DIR, 'pinned.json') SAMPLE_DATA_DIR = os.path.join(uptane.WORKING_DIR, 'samples') -TEMP_CLIENT_DIRS = [ - os.path.join(TEST_DATA_DIR, 'temp_test_secondary0'), - os.path.join(TEST_DATA_DIR, 'temp_test_secondary1'), - os.path.join(TEST_DATA_DIR, 'temp_test_secondary2'), - os.path.join(TEST_DATA_DIR, 'temp_test_partial_secondary0')] - - -# Assigns the last NUM_PARTIAL_SECONDARIES number of -# secondaries as partial verification -NUM_PARTIAL_SECONDARIES = 1 -# Indices of PV Secondaries in the list of secondaries -PV_SECONDARY1_INDICE = 3 - -# I'll initialize these in the __init__ test, and use this for the simple -# non-damaging tests so as to avoid creating objects all over again. -# Initializes the number of secondary instances to the number of -# TEMP_CLIENT_DIRS -secondary_instances = [None] * len(TEMP_CLIENT_DIRS) - -# Changing these values would require producing new signed test data from the -# Timeserver (in the case of nonce) or a Secondary (in the case of the others). +# For each Secondary instance we'll use in testing, a dictionary of the +# client directory, whether or not the instance is partial-verifying, the +# vehicle's ID, the Secondary's ID, and a reference to the instance. +# Also note the nonce we'll use when validating sample time attestation data. +# Changing the nonce or would require producing new signed test data +# from the Timeserver (in the case of nonce) or a Secondary (in the case of the +# others). nonce = 5 -vins = ['democar', 'democar', '000', '111'] -ecu_serials = ['TCUdemocar', '00000', '00000', '20000'] +TEST_INSTANCES = [ + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_secondary0'), + 'partial_verifying': False, + 'vin': 'democar', + 'ecu_serial': 'TCUdemocar', + 'instance': None}, + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_secondary1'), + 'partial_verifying': False, + 'vin': 'democar', + 'ecu_serial': '00000', + 'instance': None}, + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_secondary2'), + 'partial_verifying': False, + 'vin': '000', + 'ecu_serial': '00000', + 'instance': None}, + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_partial_secondary0'), + 'partial_verifying': True, + 'vin': 'vehicle_w_pv_bcu', + 'ecu_serial': 'pv_bcu', + 'instance': None}] + # Set starting firmware fileinfo (that this ECU had coming from the factory) # It will serve as the initial firmware state for the Secondary clients. @@ -112,9 +122,9 @@ def destroy_temp_dir(): # Clean up anything that may currently exist in the temp test directories. - for client_dir in TEMP_CLIENT_DIRS: - if os.path.exists(client_dir): - shutil.rmtree(client_dir) + for instance_data in TEST_INSTANCES: + if os.path.exists(instance_data['client_dir']): + shutil.rmtree(instance_data['client_dir']) @@ -175,9 +185,9 @@ def setUpClass(cls): # We're going to cheat in this test module for the purpose of testing # and update tuf.conf.repository_directories before each Secondary is # created, to refer to the client we're creating. - for client_dir in TEMP_CLIENT_DIRS: + for instance_data in TEST_INSTANCES: uptane.common.create_directory_structure_for_client( - client_dir, + instance_data['client_dir'], TEST_PINNING_FNAME, {'imagerepo': TEST_IMAGE_REPO_ROOT_FNAME, 'director': TEST_DIRECTOR_ROOT_FNAME}) @@ -214,8 +224,8 @@ def test_01_init(self): secondary.Secondary( full_client_dir=42, director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -231,10 +241,10 @@ def test_01_init(self): # Invalid director_repo_name with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=42, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -245,10 +255,10 @@ def test_01_init(self): # Unknown director_repo_name with self.assertRaises(uptane.Error): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name='string_that_is_not_a_known_repo_name', - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -259,10 +269,10 @@ def test_01_init(self): # Invalid VIN: with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, vin=5, - ecu_serial=ecu_serials[0], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -273,9 +283,9 @@ def test_01_init(self): # Invalid ECU Serial with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], + vin=TEST_INSTANCES[0]['vin'], ecu_serial=500, ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, @@ -286,10 +296,10 @@ def test_01_init(self): # Invalid ECU Key secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key={''}, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -300,10 +310,10 @@ def test_01_init(self): # Invalid initial time: with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time='potato', timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -314,10 +324,10 @@ def test_01_init(self): # Invalid director_public_key: with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['ecu_serial'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -332,10 +342,10 @@ def test_01_init(self): # for full verification are determined based on the root metadata file. with self.assertRaises(uptane.Error): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -344,10 +354,10 @@ def test_01_init(self): partial_verifying=False) with self.assertRaises(uptane.Error): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -359,10 +369,10 @@ def test_01_init(self): # Invalid timeserver key with self.assertRaises(tuf.FormatError): secondary.Secondary( - full_client_dir=TEMP_CLIENT_DIRS[0], + full_client_dir=TEST_INSTANCES[0]['client_dir'], director_repo_name=demo.DIRECTOR_REPO_NAME, - vin=vins[0], - ecu_serial=ecu_serials[0], + vin=TEST_INSTANCES[0]['vin'], + ecu_serial=TEST_INSTANCES[0]['ecu_serial'], ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.initial_time, # INVALID @@ -384,22 +394,23 @@ def test_01_init(self): # Initialize three clients and perform checks on each of them. - for i in range(0, len(TEMP_CLIENT_DIRS)): - client_dir = TEMP_CLIENT_DIRS[i] - ecu_serial = ecu_serials[i] - vin = vins[i] - partial_verifying_for_ecu = False - director_public_key_for_ecu = None - # Sets the last NUM_PARTIAL_SECONDARIES number of ECUs to partial - # verifying - if i == PV_SECONDARY1_INDICE: - partial_verifying_for_ecu = True + for instance_data in TEST_INSTANCES: + client_dir = instance_data['client_dir'] + ecu_serial = instance_data['ecu_serial'] + vin = instance_data['vin'] + + # Partial verification Secondaries need to be initialized with the + # Director's public key. + if instance_data['partial_verifying']: director_public_key_for_ecu = self.key_directortargets_pub + else: + director_public_key_for_ecu = None + # Try initializing each of three secondaries, expecting these calls to - # work. Save the instances for future tests as elements in a module list - # variable(secondary_instances) to save time and code. + # work. Save the instances for future tests as elements in a module + # variable (TEST_INSTANCES) to save time and code. tuf.conf.repository_directory = client_dir - secondary_instances[i] = secondary.Secondary( + instance_data['instance'] = secondary.Secondary( full_client_dir=client_dir, director_repo_name=demo.DIRECTOR_REPO_NAME, vin=vin, @@ -409,8 +420,8 @@ def test_01_init(self): timeserver_public_key=TestSecondary.key_timeserver_pub, firmware_fileinfo=factory_firmware_fileinfo, director_public_key=director_public_key_for_ecu, - partial_verifying=partial_verifying_for_ecu) - instance = secondary_instances[i] + partial_verifying=instance_data['partial_verifying']) + instance = instance_data['instance'] # Check the fields initialized in the instance to make sure they're correct. @@ -428,15 +439,6 @@ def test_01_init(self): TestSecondary.key_timeserver_pub, instance.timeserver_public_key) - #Checks the number of secondaries for PV - if i == PV_SECONDARY1_INDICE: - self.assertTrue(instance.partial_verifying) - self.assertIsNotNone(instance.director_public_key) - else: - self.assertFalse(instance.partial_verifying) - self.assertIsNone(instance.director_public_key) - - # Fields initialized, but not directly with parameters self.assertIsNone(instance.last_nonce_sent) self.assertTrue(instance.nonce_next) # Random value @@ -483,7 +485,7 @@ def test_10_nonce_rotation(self): """ # We'll just test one of the three client instances, since it shouldn't # make a difference. - instance = secondary_instances[0] + instance = TEST_INSTANCES[0]['instance'] old_nonce = instance.nonce_next @@ -506,7 +508,7 @@ def test_20_validate_time_attestation(self): # We'll just test one of the three client instances, since it shouldn't # make a difference. - instance = secondary_instances[0] + instance = TEST_INSTANCES[0]['instance'] # Try a valid time attestation first, signed by an expected timeserver key, # with an expected nonce (previously "received" from a Secondary) @@ -579,9 +581,8 @@ def test_20_validate_time_attestation(self): # Secondary that hasn't ever sent a nonce, the validation function will # reject the time attestation. (Because it doesn't matter, we'll use the # same sensible time attestation previously generated in this test func.) - instance = secondary_instances[1] with self.assertRaises(uptane.BadTimeAttestation): - instance.validate_time_attestation(time_attestation) + TEST_INSTANCES[1]['instance'].validate_time_attestation(time_attestation) # TODO: Consider other tests here. @@ -596,7 +597,7 @@ def test_25_generate_signed_ecu_manifest(self): # We'll just test one of the three client instances, since it shouldn't # make a difference. - ecu_manifest = secondary_instances[0].generate_signed_ecu_manifest() + ecu_manifest = TEST_INSTANCES[0]['instance'].generate_signed_ecu_manifest() # If the ECU Manifest is in DER format, check its format and then # convert back to JSON so that we can inspect it further. @@ -634,9 +635,9 @@ def test_40_process_metadata(self): Tests uptane.clients.secondary.Secondary::process_metadata() Tests three clients: - - secondary_instances[0]: an update is provided in Director metadata - - secondary_instances[1]: no update is provided in Director metadata - - secondary_instances[2]: no Director metadata can be retrieved + - TEST_INSTANCES[0]: an update is provided in Director metadata + - TEST_INSTANCES[1]: no update is provided in Director metadata + - TEST_INSTANCES[2]: no Director metadata can be retrieved """ # --- Test this test module's setup (defensive) @@ -656,12 +657,12 @@ def test_40_process_metadata(self): # client directories when the directories were created by the # create_directory_structure_for_client() calls in setUpClass above, and # only the root metadata file. - for client_dir in TEMP_CLIENT_DIRS: + for instance_data in TEST_INSTANCES: for repo in ['director', 'imagerepo']: self.assertEqual( ['root.' + tuf.conf.METADATA_FORMAT], sorted(os.listdir(os.path.join( - client_dir, 'metadata', repo, 'current')))) + instance_data['client_dir'], 'metadata', repo, 'current')))) # --- Set up this test @@ -677,9 +678,13 @@ def test_40_process_metadata(self): # Continue set-up followed by the test, per client. # Only tests the full verification secondaries - for i in range(0, len(TEMP_CLIENT_DIRS)-NUM_PARTIAL_SECONDARIES): - client_dir = TEMP_CLIENT_DIRS[i] - instance = secondary_instances[i] + for instance_data in TEST_INSTANCES: + + if instance_data['partial_verifying']: + continue + + client_dir = instance_data['client_dir'] + instance = instance_data['instance'] # Make sure TUF uses the right client directory. # Hack to allow multiple clients to run in the same Python process. @@ -697,7 +702,7 @@ def test_40_process_metadata(self): # Process this sample metadata. - if instance is secondary_instances[2]: + if instance_data is TEST_INSTANCES[2]: # Expect the update to fail for the third Secondary client. with self.assertRaises(tuf.NoWorkingMirrorError): instance.process_metadata(archive_fname) @@ -719,15 +724,15 @@ def test_40_process_metadata(self): # For clients 0 and 1, we expect root, snapshot, targets, and timestamp for # both director and image repo. - for client_dir in [TEMP_CLIENT_DIRS[0], TEMP_CLIENT_DIRS[1]]: + for instance_data in TEST_INSTANCES[0:2]: for repo in ['director', 'imagerepo']: self.assertEqual([ 'root.' + tuf.conf.METADATA_FORMAT, 'snapshot.' + tuf.conf.METADATA_FORMAT, 'targets.' + tuf.conf.METADATA_FORMAT, 'timestamp.' + tuf.conf.METADATA_FORMAT], - sorted(os.listdir(os.path.join(client_dir, 'metadata', repo, - 'current')))) + sorted(os.listdir(os.path.join(instance_data['client_dir'], + 'metadata', repo, 'current')))) # For client 2, we are certain that Director metadata will have failed to # update. Image Repository metadata may or may not have updated before the @@ -736,8 +741,8 @@ def test_40_process_metadata(self): # we expect to find. self.assertEqual( ['root.' + tuf.conf.METADATA_FORMAT], - sorted(os.listdir(os.path.join(TEMP_CLIENT_DIRS[2], 'metadata', - 'director', 'current')))) + sorted(os.listdir(os.path.join(TEST_INSTANCES[2]['client_dir'], + 'metadata', 'director', 'current')))) # Second: Check targets each Secondary client has been instructed to @@ -745,15 +750,15 @@ def test_40_process_metadata(self): # Client 0 should have validated expected_updated_fileinfo. self.assertEqual( expected_updated_fileinfo, - secondary_instances[0].validated_targets_for_this_ecu[0]) + TEST_INSTANCES[0]['instance'].validated_targets_for_this_ecu[0]) # Clients 1 and 2 should have no validated targets. - self.assertFalse(secondary_instances[1].validated_targets_for_this_ecu) - self.assertFalse(secondary_instances[2].validated_targets_for_this_ecu) + self.assertFalse(TEST_INSTANCES[1]['instance'].validated_targets_for_this_ecu) + self.assertFalse(TEST_INSTANCES[2]['instance'].validated_targets_for_this_ecu) # Finally, test behavior if the file we indicate does not exist. - instance = secondary_instances[0] + instance = TEST_INSTANCES[0]['instance'] with self.assertRaises(uptane.Error): instance.process_metadata('some_file_that_does_not_actually_exist.xyz') @@ -766,10 +771,8 @@ def test_45_process_partial_metadata(self): Tests uptane.clients.secondary.Secondary.process_partial_metadata() Tests PV Secondary client in 2 situations: - - secondary_instances[3]: Director's targets metadata available with - valid signatures - - secondary_instances[3]: Director's targets metadata available with - invalid signatures + - Director's targets metadata available with valid signatures + - Director's targets metadata available with invalid signatures """ # --- Test this test module's setup (defensive) # First, check the source directories, from which the temp dir is copied. @@ -795,13 +798,20 @@ def test_45_process_partial_metadata(self): replayed_metadata_path = os.path.join(SAMPLE_DATA_DIR, 'director_targets_empty_v1.' + tuf.conf.METADATA_FORMAT) + # The fourth test instance is currently our only partial verification + # test instance. If we end up with more, run a loop over the pv instances + # instead, like so: + # for instance_data in TEST_INSTANCES: + # if not instance_data['partial_verification']: + # continue + client_dir = TEST_INSTANCES[3]['client_dir'] + instance = TEST_INSTANCES[3]['instance'] + # director_targets_metadata_path is where the partial verification Secondary # client stores the Director Targets metadata it gets from the Primary, # which it then will validate. - pv_secondary_dir = TEMP_CLIENT_DIRS[PV_SECONDARY1_INDICE] - instance = secondary_instances[PV_SECONDARY1_INDICE] - director_targets_metadata_path = os.path.join(pv_secondary_dir, - 'metadata', 'director_targets.' + tuf.conf.METADATA_FORMAT) + director_targets_metadata_path = os.path.join( + client_dir, 'metadata', 'director_targets.' + tuf.conf.METADATA_FORMAT) # First, test behavior if the file we indicate does not exist. with self.assertRaises(uptane.Error): From 83facbef34687fcbfc62b8602cae57802803268b Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 16:31:01 -0500 Subject: [PATCH 42/43] In test_secondary, give pv Sec metadata pointing to test firmware, instead of to an absent file. This way, we can also test the validation of that firmware when we present it to the partial verification Secondary. (Previously, the metadata was validated and firmware itself was not checked against this validated metadat during the testing of the partial verification client.) Signed-off-by: Sebastien Awwad --- samples/director_targets_pv_bcu_v2.der | Bin 0 -> 296 bytes samples/director_targets_pv_bcu_v2.json | 30 ++++++++++++++++++++++++ samples/director_targets_v2.der | Bin 299 -> 0 bytes samples/director_targets_v2.json | 30 ------------------------ tests/test_secondary.py | 2 +- 5 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 samples/director_targets_pv_bcu_v2.der create mode 100644 samples/director_targets_pv_bcu_v2.json delete mode 100644 samples/director_targets_v2.der delete mode 100644 samples/director_targets_v2.json diff --git a/samples/director_targets_pv_bcu_v2.der b/samples/director_targets_pv_bcu_v2.der new file mode 100644 index 0000000000000000000000000000000000000000..ec1aaa0875652d93c3bf7508b10241883d6b6ab3 GIT binary patch literal 296 zcmV+@0oVR8f&nC;fvbb$d6f&l`faxf==0Rf>Rfgt+|rD>W@duRZk?Q026%K*czR&bIV;kg30DkIHC zb+Ir)4mks}FHGXqvQVoV!qY*N zy3cY8PYUfe9{AOo$15Abo;o5rUWd)02!aN1c3)y+b%6l^qHQp1pdx`FV+{3#^O8dg zAsM7OUO8e;hV|veH$?}hiN0L(Tkw$9fdK)cLV-X$8|ff@vw)8*mq#%&NhyUzzDvNO uk8-D>qE*Cc10mh74S};P7Z_hdzGgC|oY9g$d(otD`t_|GfHGfwF)B0f7Ws*c;)30Rp3eqoIMIfB^xaftoOZm!N@*fDbQbX>x6M zVRB_MGcIXuXMq6}f&l`faxf==0Rf>RfgsxM!c|q7xaX)VZxcDy^cgg3SD@>9IqqXL zxbg*AS5h!XfB^%cLV-XPaFcq3>E}*0_2p`hg&4^m7$uDV$gJW?*{tz%(S%)s040O5 zwKH?Kp5IbQO;S%L8TIjrZ!m=ejqIr`l>~;N2Z9ALFfcGMfdK)cZ7^$~B7q=d4E2Qb zl0yq28KgO0Ibu$R_2tDkMF*#ezFhNL@Q~Jl0Rf^yfj~J5(BHoLvTn($mL_qat>P`F x_?>v%hFx%q*3gY7+y2L+dLSZdMSrY;n?@hRkBaFAj>&_R#H-J{iT?}M#tf!Eh%x{G diff --git a/samples/director_targets_v2.json b/samples/director_targets_v2.json deleted file mode 100644 index ba5acea..0000000 --- a/samples/director_targets_v2.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "signatures": [ - { - "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", - "method": "ed25519", - "sig": "9560312be8ebd451c689724a3ec2fcbdb7ba708b27669861e0146bd61a2a15f04baf90edc75edd2e437ac680a5d2b3d76b3e4b9766a8a961108af6b4e501950e" - } - ], - "signed": { - "_type": "Targets", - "delegations": { - "keys": {}, - "roles": [] - }, - "expires": "2021-01-10T15:14:21Z", - "targets": { - "/firmware13.img": { - "custom": { - "ecu_serial": "20000" - }, - "hashes": { - "sha256": "daeec2555599b8e7a82b6f1339d5f419346b57a0eb7a39ee6334b8f205595752", - "sha512": "1570937a84e9e74e35f5e56a8f8518c91e18258cffc8ace249d9acf173d1845d82002583b1b53373b79edf52494d524f2619f5f1896f3085038deca92c950486" - }, - "length": 20 - } - }, - "version": 2 - } -} diff --git a/tests/test_secondary.py b/tests/test_secondary.py index bab18e4..b62de9f 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -787,7 +787,7 @@ def test_45_process_partial_metadata(self): sorted(os.listdir(data_directory))) working_metadata_path = os.path.join(SAMPLE_DATA_DIR, - 'director_targets_v2.' + tuf.conf.METADATA_FORMAT) + 'director_targets_pv_bcu_v2.' + tuf.conf.METADATA_FORMAT) bad_sig_metadata_path = os.path.join(SAMPLE_DATA_DIR, 'director_targets_bad_sig_v2.' + tuf.conf.METADATA_FORMAT) From 43c1262cca828122cd9bcc3e33253e489800e72a Mon Sep 17 00:00:00 2001 From: Sebastien Awwad Date: Thu, 22 Feb 2018 16:34:24 -0500 Subject: [PATCH 43/43] In test_secondary, test p-v Sec's validation of an image as well as just the f-v Sec's validation of images. Also closes a potential gap in testing whereby the second and third tests for full verification client validation of images were pre-empted by missing files leading to similar errors. Signed-off-by: Sebastien Awwad --- tests/test_secondary.py | 54 ++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index b62de9f..b910fe9 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -883,24 +883,56 @@ def test_45_process_partial_metadata(self): def test_50_validate_image(self): - image_fname = 'TCU1.1.txt' + # In these tests, the full verification Secondaries were or were not given + # instructions to install TCU1.1.txt, and the partial verification + # Secondary was given an instruction to install BCU1.0.txt. + fv_image_fname = 'TCU1.1.txt' + pv_image_fname = 'BCU1.0.txt' sample_image_location = os.path.join(demo.DEMO_DIR, 'images') - client_unverified_targets_dir = TEMP_CLIENT_DIRS[0] + '/unverified_targets' + fv_client_unverified_targets_dir = TEST_INSTANCES[0]['client_dir'] + \ + '/unverified_targets' + pv_client_unverified_targets_dir = TEST_INSTANCES[3]['client_dir'] + \ + '/unverified_targets' - if os.path.exists(client_unverified_targets_dir): - shutil.rmtree(client_unverified_targets_dir) - os.mkdir(client_unverified_targets_dir) - shutil.copy( - os.path.join(sample_image_location, image_fname), - client_unverified_targets_dir) + # Copy the firmware into the Secondary's unverified targets directory. + # (This is what the Secondary would do when receiving the file from + # the Primary.) + # Delete and recreate the unverified targets directory first. + for instance_data in TEST_INSTANCES: + client_unverified_targets_dir = os.path.join( + instance_data['client_dir'], 'unverified_targets') + + if os.path.exists(client_unverified_targets_dir): + shutil.rmtree(client_unverified_targets_dir) + os.mkdir(client_unverified_targets_dir) + + if instance_data['partial_verifying']: + image_fname = pv_image_fname + else: + image_fname = fv_image_fname - secondary_instances[0].validate_image(image_fname) + shutil.copy( + os.path.join(sample_image_location, image_fname), + client_unverified_targets_dir) + + + # For each Secondary, try validating the appropriate firmware image. + # Secondaries 0-2 are running full verification. + TEST_INSTANCES[0]['instance'].validate_image(fv_image_fname) with self.assertRaises(uptane.Error): - secondary_instances[1].validate_image(image_fname) + TEST_INSTANCES[1]['instance'].validate_image(fv_image_fname) with self.assertRaises(uptane.Error): - secondary_instances[2].validate_image(image_fname) + TEST_INSTANCES[2]['instance'].validate_image(fv_image_fname) + + # Secondary 3 is running partial verification and was given metadata + # indicating the following firmware: + shutil.copy( + os.path.join(sample_image_location, pv_image_fname), + client_unverified_targets_dir) + TEST_INSTANCES[3]['instance'].validate_image(pv_image_fname) +