diff --git a/demo/demo_primary.py b/demo/demo_primary.py index 7293856..2ca6061 100644 --- a/demo/demo_primary.py +++ b/demo/demo_primary.py @@ -138,7 +138,7 @@ def clean_slate( director_repo_name=demo.DIRECTOR_REPO_NAME, vin=_vin, ecu_serial=_ecu_serial, - primary_key=ecu_key, + ecu_key=ecu_key, time=clock, timeserver_public_key=key_timeserver_pub) @@ -381,7 +381,7 @@ def register_self_with_director(): print('Registering Primary ECU Serial and Key with Director.') server.register_ecu_serial( primary_ecu.ecu_serial, - uptane.common.public_key_from_canonical(primary_ecu.primary_key), + uptane.common.public_key_from_canonical(primary_ecu.ecu_key), _vin, True) print(GREEN + 'Primary has been registered with the Director.' + ENDCOLORS) @@ -494,7 +494,7 @@ def get_metadata_for_ecu(ecu_serial, force_partial_verification=False): fname = None if force_partial_verification: - fname = primary_ecu.get_partial_metadata_fname() + fname = primary_ecu.get_partial_metadata_archive_fname() else: # Note that in Python 2.7.4 and later, unzipping should prevent files from diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index e3f50b2..5c45b4e 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -63,6 +63,7 @@ _ecu_serial = 'TCUdemocar' _primary_host = demo.PRIMARY_SERVER_HOST _primary_port = demo.PRIMARY_SERVER_DEFAULT_PORT +_partial_verifying = False firmware_filename = 'secondary_firmware.txt' current_firmware_fileinfo = {} secondary_ecu = None @@ -79,7 +80,8 @@ def clean_slate( vin=_vin, ecu_serial=_ecu_serial, primary_host=None, - primary_port=None): + primary_port=None, + partial_verifying=_partial_verifying): """ """ @@ -88,12 +90,14 @@ 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 @@ -104,6 +108,12 @@ def clean_slate( CLIENT_DIRECTORY = os.path.join( uptane.WORKING_DIR, CLIENT_DIRECTORY_PREFIX + demo.get_random_string(5)) + # If secondary is partial verification then it would need director public key + if _partial_verifying: + key_director_pub = demo.import_public_key('director') + else: + key_director_pub = None + # Load the public timeserver key. key_timeserver_pub = demo.import_public_key('timeserver') @@ -154,7 +164,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,7 +314,8 @@ 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_from_primary = pserver.get_metadata( + secondary_ecu.ecu_serial, secondary_ecu.partial_verifying) # Verify the time attestation and internalize the time (if verified, the time # will be used in place of system time to perform future metadata expiration @@ -326,7 +339,7 @@ def update_cycle(): secondary_ecu.full_client_dir, 'metadata_archive.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. diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.der deleted file mode 100644 index d836c76..0000000 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.der and /dev/null differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip index f2fb767..6773446 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive.zip differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der index 1e9e82a..8c25a3b 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json index 790a3a4..ad86732 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "e9855e5171934d56a78033cead3dc217d6df3730f9c668742346a4e66f0d1141fe7283a21964e0c35163e76b6103e36a04d44f1b0799fe34af45c65f32f38b09" + "sig": "1749f961bda975ef8db96b56b8b61465ef36fbbb45ce9c92248303851e36c21bd28d1cadfff9a12a0b1d87c3ea9294736d976a5a00b83167bc99c6b2a3a8860d" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "keys": { "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { "keyid_hash_algorithms": [ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der index ff0488e..e81cacc 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.der @@ -1,4 +1,3 @@ -0֠f{' XV0 targets.der=root.derw'0% - k8} -"Lc~rI9A9i0g <=3_6T`GࡍY^SkVx -@*Ed5[{%UYQ)'gH\ǃL8 s|jz&j \ No newline at end of file +0֠fXV0 targets.der=root.derw'0% + ;_k.c_f? l½j8i0g <=3_6T`GࡍY^SkVx +@~`)vzoB87obl"x/95pp( \ No newline at end of file diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json index 7623663..b6040f0 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/snapshot.json @@ -3,16 +3,16 @@ { "keyid": "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97", "method": "ed25519", - "sig": "7606de7a3cb4d899755d8fe085f8834a356ca81b65814e89aa5062faa78360ff12b9977825fa9e14baf3daf63c4da2c354ec0fe233a98d3aac63d5758caab60e" + "sig": "dbf828452d952b1423b6797bceb6f1eea0f049cf3da73f42af85c032b3a4b3265d9eb164616ea94afe8bcf0d2c972a0abdf11a7a0a0107711bbf47eb9df7ff04" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "meta": { "root.json": { "hashes": { - "sha256": "2a6db46564a0fbc905bf7a36eddc172f1ce3a52871f18f2594e5160b2321e62d" + "sha256": "983203b28c67ea490db40bc82801aa771c9522ccbbc33bcf3239e37eef95523d" }, "length": 2120, "version": 1 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der index d836c76..1a9de0d 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json index 8614ec3..6b9c9a8 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "263a6d873455bac478366d92edc04cac55cc0e545b5f4249cf696254263d58de6a225446ce96725bccb83ffdaab548fcd89cb790020039ef336fb422364c1800" + "sig": "df56f77ffad98db3152a9c27fda8220a60373174fb5b5173039bf5eb9d2a7fb23926f37d0b41f67c72a29ad20d3c908db327982c59852e986d270a3336432a03" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "targets": {}, "version": 1 } diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der index e1e1df6..f047f72 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json index b08013e..82d839b 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/director/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc", "method": "ed25519", - "sig": "3500dd4af7002c2a7d36c1a4b546b87979805a5c31f6038b39ca19d5d61868b5d3cc8c96c93eb44c3ed862859f45739e926626b73b7beb2d37ceeb83b32c8b0c" + "sig": "4d29caea1f135790c99f5112c89833c363e23a72f082dee707bf9723a9c45bda47f8bfd8711a646139e16d892c9a0ea99b86c85f021ce9db28ee70ae3d5c6e07" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "114817f21566942dbcdafe97e3d1aca1660db86d8920a872c65b76d9890409de" + "sha256": "9d98d3a1a89a277eb23fb3237709318fbea54249c539dbf5b53eb8318c75847a" }, "length": 594, "version": 1 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der index 6fea55f..5c932fb 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json index cd2cd95..8375b5d 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", "method": "ed25519", - "sig": "bdbbbfa6b75e98bbc5f04549f1b181e39cc7c8ad8bd86a41c061825e01ed5d2cf1a294cb45f7441cb15fe0302264a98c7813af699c12ba25ccee08878436c20b" + "sig": "f08d8dfe263e55facaaa16eda78a8e51fbbaa2e36ae47b93a8103ca0982dc3deb988f7a1aee5c805a322232b01299c5919b3f8cb6bf04cca7a68e9ac3903d309" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "keys": { "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a": { "keyid_hash_algorithms": [ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.der index 7f5ca7f..be72d96 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.json index d5ce843..b0f4698 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/snapshot.json @@ -3,16 +3,16 @@ { "keyid": "aaf05f8d054f8068bf6cb46beed7c824e2560802df462fc8681677586582ca99", "method": "ed25519", - "sig": "8cb2bff8f52a5aed8979a9c1ebfa047fb4ee58b63830037593bd240ceaafbae274c80c9d59aa3b6fba2bdd6de3c535e3230de2a51509042ca2f7d61523065709" + "sig": "5259eabac762bf61e0edce451f0645861ee79f3d6654d03c484badbff95e6c216daf82cc9a9a6285540a1126486fc5f3cf6a35985b7dc62ec32e4f2ab882d600" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "root.json": { "hashes": { - "sha256": "69385580f90dcd47d7d309070f65515188d392b41416d1efeb330978b77e4a96" + "sha256": "46f623b52e1e0d17bef556175227c90ea0d79c3f4e33957294fe5b1fdbddae76" }, "length": 2120, "version": 1 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.der index 8e2204a..ad3d257 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json index 9275521..ee1c00e 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596", "method": "ed25519", - "sig": "aa5c4bd41dbb8bcf5fdc6fd44ec6b42998488fb1849cc9634e58e514dc5f00f7a59e75216db325f07bfdaba72558a9a4dcfe694f76c8e9af2980477a2893b208" + "sig": "6c300b24ffb94f9d1d3465e3e9a877a359c5e5316e3edfd86154eeee08503e111fa23c8b7ca8af2af74747e701272999e61b4930b98a15a41e4a6d20e781490f" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "targets": { "/BCU1.0.txt": { "hashes": { diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der index 78e17e8..93282ab 100644 Binary files a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der and b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json index 1a34ad6..3d490d5 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/full_metadata_archive/imagerepo/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a", "method": "ed25519", - "sig": "8aa14b371d10f81a5f4232967d682ef254f5a92221b5cd224811e053d5a8bd99cc1298984c284580684a958eec2f40a6eaf0d8756bd950e5b3714736befda805" + "sig": "0c2a95485dca56dadf4be8f8f015de79d92e015427c79a5ff134df63f6a81b19abd2bec2fd85840a7624c2409faaf694cbec2467d9012a42660e223000eb540d" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "d37a3f9b41bd7ad4c87bc978f0341cbeb081bdee0891f087f33d34963c565e16" + "sha256": "d637c37e4417c7649b275403eacf3bf2c0db913bc72ac2847e55968d07ea0716" }, "length": 594, "version": 1 diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive.zip b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive.zip new file mode 100644 index 0000000..0d6adf4 Binary files /dev/null and b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive.zip differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.der new file mode 100644 index 0000000..1a9de0d Binary files /dev/null and b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.json similarity index 59% rename from samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json rename to samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.json index 8614ec3..6b9c9a8 100644 --- a/samples/metadata_samples_long_expiry/initial_w_no_update/director_targets.json +++ b/samples/metadata_samples_long_expiry/initial_w_no_update/partial_metadata_archive/director/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "263a6d873455bac478366d92edc04cac55cc0e545b5f4249cf696254263d58de6a225446ce96725bccb83ffdaab548fcd89cb790020039ef336fb422364c1800" + "sig": "df56f77ffad98db3152a9c27fda8220a60373174fb5b5173039bf5eb9d2a7fb23926f37d0b41f67c72a29ad20d3c908db327982c59852e986d270a3336432a03" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "targets": {}, "version": 1 } diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.der deleted file mode 100644 index 09869c3..0000000 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.der and /dev/null differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive.zip b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive.zip index acc2e4b..e44baac 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive.zip and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive.zip differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.der index 1e9e82a..8c25a3b 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json index 790a3a4..ad86732 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "fdba7eaa358fa5a8113a789f60c4a6ce29c4478d8d8eff3e27d1d77416696ab2", "method": "ed25519", - "sig": "e9855e5171934d56a78033cead3dc217d6df3730f9c668742346a4e66f0d1141fe7283a21964e0c35163e76b6103e36a04d44f1b0799fe34af45c65f32f38b09" + "sig": "1749f961bda975ef8db96b56b8b61465ef36fbbb45ce9c92248303851e36c21bd28d1cadfff9a12a0b1d87c3ea9294736d976a5a00b83167bc99c6b2a3a8860d" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "keys": { "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6": { "keyid_hash_algorithms": [ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der index c5a2945..60a7e7d 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json index 49b5157..ce09e30 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/snapshot.json @@ -3,16 +3,16 @@ { "keyid": "f93cfcf33d335ff43654ec6047e0a18dd5595ee3de53136b94c9c756788a0f97", "method": "ed25519", - "sig": "701d882716b54c0953d6811e0d4e5cfe788d75f4d0c425af9b2ec9bd8e3abec3f2ac707575680601a1b983b11aa290ec7add7070734a6a05b2a1117a9a31320c" + "sig": "df224bc6165f35638d37cbd207d613660abf62abfa9c6d90b59f8989ad26e1f1661118863589ecaa29a3b36bff090c3dfec05106cb906c5ff7c08303ea66bc06" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "meta": { "root.json": { "hashes": { - "sha256": "2a6db46564a0fbc905bf7a36eddc172f1ce3a52871f18f2594e5160b2321e62d" + "sha256": "983203b28c67ea490db40bc82801aa771c9522ccbbc33bcf3239e37eef95523d" }, "length": 2120, "version": 1 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der index 09869c3..ad83b24 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.json index f883915..763aac7 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "e97a2e9d819555cc2c8af799cb2020857a0217ca5246d3365e3d881d9f53aa94ded9ef6c64caa8b606728b52451555c3a79500c266163b9a66933bde7012c302" + "sig": "f8ed79a8d122eb1eaf1132c980989480c36a4393ed1e6d63fcfa27623b996d395bc1db150692dd339a003a7338bdafacdae03204ebaeb9d64c10bb050715e003" } ], "signed": { @@ -12,8 +12,18 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "targets": { + "/BCU1.1.txt": { + "custom": { + "ecu_serial": "BCUdemocar" + }, + "hashes": { + "sha256": "1eb6fa5c6bb606c5326d6ef0ff05f5fcefde4e50c7daea530978090778b38bf4", + "sha512": "9727058c2ba828fdd2fc5ae02f52c10e47404283f92df3539989e2ada3cf7e85a9772faed1bd0bad3fc2bd8f6e5d15b976b8e832dd46874be72b994bc57a62a0" + }, + "length": 18 + }, "/TCU1.1.txt": { "custom": { "ecu_serial": "TCUdemocar" diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.der index 5179294..7277911 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.json index 5fb7a72..53a183c 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/director/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "da9c65c96c5c4072f6984f7aa81216d776aca6664d49cb4dfafbc7119320d9cc", "method": "ed25519", - "sig": "2e170caedf043a5df86f731d6b60eed84766ee66a9cb6a3ea42423a7a8d64dfedf1dec0c7fc1b56cd6479bfcc57c6f5861ded88446aaebb9f88b016020dd2603" + "sig": "19ecc7d5ae73238e91bc15577837d678d845f3a9efb0f23f908d600f6aa870198a391494f2440b15c9b460eed6ded0e30eaf4ffa8b8ae0eb52da594deb46b008" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "d6a27db7727136afedc07d1e34bb078a6436c6779fa13d324942f135e04d0710" + "sha256": "36020d21bbdaa8581156a333541367100c2409c48f35e018888c4fd5c9b1f9d7" }, "length": 594, "version": 2 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.der index 6fea55f..5c932fb 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json index cd2cd95..8375b5d 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/root.json @@ -3,7 +3,7 @@ { "keyid": "94c836f0c45168f0a437eef0e487b910f58db4d462ae457b5730a4487130f290", "method": "ed25519", - "sig": "bdbbbfa6b75e98bbc5f04549f1b181e39cc7c8ad8bd86a41c061825e01ed5d2cf1a294cb45f7441cb15fe0302264a98c7813af699c12ba25ccee08878436c20b" + "sig": "f08d8dfe263e55facaaa16eda78a8e51fbbaa2e36ae47b93a8103ca0982dc3deb988f7a1aee5c805a322232b01299c5919b3f8cb6bf04cca7a68e9ac3903d309" } ], "signed": { @@ -12,7 +12,7 @@ "gz" ], "consistent_snapshot": false, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "keys": { "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a": { "keyid_hash_algorithms": [ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.der index 7f5ca7f..be72d96 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.json index d5ce843..b0f4698 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/snapshot.json @@ -3,16 +3,16 @@ { "keyid": "aaf05f8d054f8068bf6cb46beed7c824e2560802df462fc8681677586582ca99", "method": "ed25519", - "sig": "8cb2bff8f52a5aed8979a9c1ebfa047fb4ee58b63830037593bd240ceaafbae274c80c9d59aa3b6fba2bdd6de3c535e3230de2a51509042ca2f7d61523065709" + "sig": "5259eabac762bf61e0edce451f0645861ee79f3d6654d03c484badbff95e6c216daf82cc9a9a6285540a1126486fc5f3cf6a35985b7dc62ec32e4f2ab882d600" } ], "signed": { "_type": "Snapshot", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "root.json": { "hashes": { - "sha256": "69385580f90dcd47d7d309070f65515188d392b41416d1efeb330978b77e4a96" + "sha256": "46f623b52e1e0d17bef556175227c90ea0d79c3f4e33957294fe5b1fdbddae76" }, "length": 2120, "version": 1 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.der index 8e2204a..ad3d257 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json index 9275521..ee1c00e 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "c24b457b2ca4b3c2f415efdbbebb914a0d05c5345b9889bda044362589d6f596", "method": "ed25519", - "sig": "aa5c4bd41dbb8bcf5fdc6fd44ec6b42998488fb1849cc9634e58e514dc5f00f7a59e75216db325f07bfdaba72558a9a4dcfe694f76c8e9af2980477a2893b208" + "sig": "6c300b24ffb94f9d1d3465e3e9a877a359c5e5316e3edfd86154eeee08503e111fa23c8b7ca8af2af74747e701272999e61b4930b98a15a41e4a6d20e781490f" } ], "signed": { @@ -12,7 +12,7 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "targets": { "/BCU1.0.txt": { "hashes": { diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der index 78e17e8..93282ab 100644 Binary files a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der and b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json index 1a34ad6..3d490d5 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/full_metadata_archive/imagerepo/metadata/timestamp.json @@ -3,16 +3,16 @@ { "keyid": "6fcd9a928358ad8ca7e946325f57ec71d50cb5977a8d02c5ab0de6765fef040a", "method": "ed25519", - "sig": "8aa14b371d10f81a5f4232967d682ef254f5a92221b5cd224811e053d5a8bd99cc1298984c284580684a958eec2f40a6eaf0d8756bd950e5b3714736befda805" + "sig": "0c2a95485dca56dadf4be8f8f015de79d92e015427c79a5ff134df63f6a81b19abd2bec2fd85840a7624c2409faaf694cbec2467d9012a42660e223000eb540d" } ], "signed": { "_type": "Timestamp", - "expires": "2037-09-28T12:46:18Z", + "expires": "2038-01-18T03:14:07Z", "meta": { "snapshot.json": { "hashes": { - "sha256": "d37a3f9b41bd7ad4c87bc978f0341cbeb081bdee0891f087f33d34963c565e16" + "sha256": "d637c37e4417c7649b275403eacf3bf2c0db913bc72ac2847e55968d07ea0716" }, "length": 594, "version": 1 diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive.zip b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive.zip new file mode 100644 index 0000000..ee277c0 Binary files /dev/null and b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive.zip differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.der b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.der new file mode 100644 index 0000000..ad83b24 Binary files /dev/null and b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.der differ diff --git a/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.json b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.json similarity index 53% rename from samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.json rename to samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.json index f883915..763aac7 100644 --- a/samples/metadata_samples_long_expiry/update_to_one_ecu/director_targets.json +++ b/samples/metadata_samples_long_expiry/update_to_one_ecu/partial_metadata_archive/director/metadata/targets.json @@ -3,7 +3,7 @@ { "keyid": "630cf584f392430b2119a4395e39624e86f5e5c5374507a789be5cf35bf090d6", "method": "ed25519", - "sig": "e97a2e9d819555cc2c8af799cb2020857a0217ca5246d3365e3d881d9f53aa94ded9ef6c64caa8b606728b52451555c3a79500c266163b9a66933bde7012c302" + "sig": "f8ed79a8d122eb1eaf1132c980989480c36a4393ed1e6d63fcfa27623b996d395bc1db150692dd339a003a7338bdafacdae03204ebaeb9d64c10bb050715e003" } ], "signed": { @@ -12,8 +12,18 @@ "keys": {}, "roles": [] }, - "expires": "2037-09-28T12:46:29Z", + "expires": "2038-01-18T03:14:15Z", "targets": { + "/BCU1.1.txt": { + "custom": { + "ecu_serial": "BCUdemocar" + }, + "hashes": { + "sha256": "1eb6fa5c6bb606c5326d6ef0ff05f5fcefde4e50c7daea530978090778b38bf4", + "sha512": "9727058c2ba828fdd2fc5ae02f52c10e47404283f92df3539989e2ada3cf7e85a9772faed1bd0bad3fc2bd8f6e5d15b976b8e832dd46874be72b994bc57a62a0" + }, + "length": 18 + }, "/TCU1.1.txt": { "custom": { "ecu_serial": "TCUdemocar" diff --git a/tests/test_primary.py b/tests/test_primary.py index f7087a9..6c29745 100644 --- a/tests/test_primary.py +++ b/tests/test_primary.py @@ -176,7 +176,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=5, # INVALID ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -188,7 +188,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=500, # INVALID - primary_key=TestPrimary.ecu_key, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -200,7 +200,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key={''}, # INVALID + ecu_key={''}, # INVALID time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -212,7 +212,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, + ecu_key=TestPrimary.ecu_key, time='invalid because this is not a time', # INVALID timeserver_public_key=TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -224,7 +224,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.initial_time, # INVALID my_secondaries=[]) @@ -235,7 +235,7 @@ def test_01_init(self): director_repo_name=5, #INVALID vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key = TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -246,7 +246,7 @@ def test_01_init(self): director_repo_name= "invalid", #INVALID vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key = TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -261,7 +261,7 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, - primary_key=TestPrimary.ecu_key, + ecu_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub) @@ -272,7 +272,7 @@ def test_01_init(self): self.assertEqual([], TestPrimary.instance.nonces_sent) self.assertEqual(VIN, TestPrimary.instance.vin) self.assertEqual(PRIMARY_ECU_SERIAL, TestPrimary.instance.ecu_serial) - self.assertEqual(TestPrimary.ecu_key, TestPrimary.instance.primary_key) + self.assertEqual(TestPrimary.ecu_key, TestPrimary.instance.ecu_key) self.assertEqual(dict(), TestPrimary.instance.ecu_manifests) self.assertEqual( TestPrimary.instance.full_client_dir, TEMP_CLIENT_DIR) @@ -873,11 +873,11 @@ def test_61_get_full_metadata_archive_fname(self): - def test_62_get_partial_metadata_fname(self): + def test_62_get_partial_metadata_archive_fname(self): # TODO: More thorough tests. - fname = TestPrimary.instance.get_partial_metadata_fname() + fname = TestPrimary.instance.get_partial_metadata_archive_fname() self.assertTrue(fname) diff --git a/tests/test_secondary.py b/tests/test_secondary.py index cdd92bd..58e9f6e 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -57,15 +57,54 @@ os.path.join(TEST_DATA_DIR, 'temp_test_secondary1'), os.path.join(TEST_DATA_DIR, 'temp_test_secondary2')] -# 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] -# 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'] -ecu_serials = ['TCUdemocar', '00000', '00000'] + +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': 'democar', + 'ecu_serial': 'BCUdemocar', + 'instance': None}, + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_partial_secondary1'), + 'partial_verifying': True, + 'vin': 'democar', + 'ecu_serial': 'pv_00000', + 'instance': None}, + { + 'client_dir': os.path.join(TEST_DATA_DIR, 'temp_partial_secondary2'), + 'partial_verifying': True, + 'vin': '000', + 'ecu_serial': 'pv_00000', + '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. @@ -77,7 +116,7 @@ 'sha256': '6b9f987226610bfed08b824c93bf8b2f59521fce9a2adef80c495f363c1c9c44'}, 'length': 37}} -expected_updated_fileinfo = { +fv_expected_updated_fileinfo = { 'filepath': '/TCU1.1.txt', 'fileinfo': { 'custom': {'ecu_serial': 'TCUdemocar'}, @@ -86,12 +125,21 @@ 'sha256': '56d7cd56a85e34e40d005e1f79c0e95d6937d5528ac0b301dbe68d57e03a5c21'}, 'length': 17}} +pv_expected_updated_fileinfo = { + 'filepath': "/BCU1.1.txt", + 'fileinfo': { + "custom": {"ecu_serial": "BCUdemocar"}, + "hashes": { + "sha256": "1eb6fa5c6bb606c5326d6ef0ff05f5fcefde4e50c7daea530978090778b38bf4", + "sha512": "9727058c2ba828fdd2fc5ae02f52c10e47404283f92df3539989e2ada3cf7e85a9772faed1bd0bad3fc2bd8f6e5d15b976b8e832dd46874be72b994bc57a62a0"}, + "length": 18}} + 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']) @@ -152,9 +200,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}) @@ -191,8 +239,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, @@ -208,10 +256,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, @@ -222,10 +270,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, @@ -236,10 +284,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, @@ -250,9 +298,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, @@ -264,10 +312,10 @@ def test_01_init(self): # Invalid ECU 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={''}, time=TestSecondary.initial_time, timeserver_public_key=TestSecondary.key_timeserver_pub, @@ -278,10 +326,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, @@ -292,10 +340,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]['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, @@ -310,10 +358,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, @@ -322,10 +370,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, @@ -337,10 +385,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 @@ -362,16 +410,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] + 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, @@ -380,11 +435,9 @@ 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) - - instance = secondary_instances[i] - + director_public_key=director_public_key_for_ecu, + partial_verifying=instance_data['partial_verifying']) + instance = instance_data['instance'] # Check the fields initialized in the instance to make sure they're correct. # Fields initialized from parameters @@ -399,11 +452,9 @@ 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) # 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) @@ -448,7 +499,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 @@ -471,7 +522,7 @@ def test_20_update_time(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 good time attestation first, signed by an expected timeserver key, # with an expected nonce (previously "received" from a Secondary) @@ -574,7 +625,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. @@ -612,9 +663,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) @@ -623,48 +674,64 @@ 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 repo in ['director', 'imagerepo']: + for instance_data in TEST_INSTANCES: + if instance_data['partial_verifying']: + repo_list = ['director'] + else: + repo_list = ['director', 'imagerepo'] + for repo in repo_list: 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 # Location of the sample Primary-produced metadata archive - sample_archive_fname = os.path.join( + sample_fv_archive_fname = os.path.join( uptane.WORKING_DIR, 'samples', 'metadata_samples_long_expiry', 'update_to_one_ecu', 'full_metadata_archive.zip') + sample_pv_archive_fname = os.path.join( + uptane.WORKING_DIR, 'samples', 'metadata_samples_long_expiry', + 'update_to_one_ecu', 'partial_metadata_archive.zip') + - assert os.path.exists(sample_archive_fname), 'Cannot test ' \ + assert os.path.exists(sample_fv_archive_fname), 'Cannot test ' \ 'process_metadata; unable to find expected sample metadata archive' + \ - ' at ' + repr(sample_archive_fname) + ' at ' + repr(sample_fv_archive_fname) + assert os.path.exists(sample_pv_archive_fname), 'Cannot test ' \ + 'process_metadata; unable to find expected sample metadata archive' + \ + ' at ' + repr(sample_pv_archive_fname) # Continue set-up followed by the test, per client. - for i in range(0, len(TEMP_CLIENT_DIRS)): - client_dir = TEMP_CLIENT_DIRS[i] - instance = secondary_instances[i] + # Only tests the full verification secondaries + for instance_data in TEST_INSTANCES: + + 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. # See comments in SetUpClass() method. tuf.conf.repository_directory = client_dir - # Location in the client directory to which we'll copy the archive. - archive_fname = os.path.join(client_dir, 'full_metadata_archive.zip') - - # Copy the sample archive into place in the client directory. - shutil.copy(sample_archive_fname, archive_fname) + # Getting the location in the client directory to which we'll copy the archive + # and then copy the sample archive into place in the client directory. + if instance_data['partial_verifying']: + archive_fname = os.path.join(client_dir, 'partial_metadata_archive.zip') + shutil.copy(sample_pv_archive_fname, archive_fname) + else: + archive_fname = os.path.join(client_dir, 'full_metadata_archive.zip') + shutil.copy(sample_fv_archive_fname, archive_fname) # --- Perform the test - # Process this sample metadata. + # Process this sample metadata - if instance is secondary_instances[2]: - # Expect the update to fail for the third Secondary client. + if instance_data in [TEST_INSTANCES[2], TEST_INSTANCES[5]]: + # Expect the update to fail for the third and fifth Secondary client. with self.assertRaises(tuf.NoWorkingMirrorError): instance.process_metadata(archive_fname) continue @@ -673,8 +740,14 @@ def test_40_process_metadata(self): instance.process_metadata(archive_fname) # Make sure the archive of unverified metadata was expanded - for repo in ['director', 'imagerepo']: - for role in ['root', 'snapshot', 'targets', 'timestamp']: + if instance_data['partial_verifying']: + repo_list = ['director'] + roles_list = ['targets'] + else: + repo_list = ['director', 'imagerepo'] + roles_list = ['root', 'snapshot', 'targets', 'timestamp'] + for repo in repo_list: + for role in roles_list: self.assertTrue(os.path.exists(client_dir + '/unverified/' + repo + '/metadata/' + role + '.' + tuf.conf.METADATA_FORMAT)) @@ -685,41 +758,60 @@ 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 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')))) - - # For client 2, we are certain that Director metadata will have failed to + # For clients 3 and 4, we expect root and targets for director repo + for instance_data in (TEST_INSTANCES[0:2] + TEST_INSTANCES[3:5]): + if instance_data['partial_verifying']: + repo_list = ['director'] + # Both root and targets as there is root file needed to establish a root + # of trust while shipping the ECU + roles_list = ['root','targets'] + else: + repo_list = ['director', 'imagerepo'] + roles_list = ['root', 'snapshot', 'targets', 'timestamp'] + roles_in_repo_directory = [] + + for role in roles_list: + roles_in_repo_directory.append( + role + '.' + tuf.conf.METADATA_FORMAT) + + for repo in repo_list: + self.assertEqual(roles_in_repo_directory, + sorted(os.listdir(os.path.join(instance_data['client_dir'], + 'metadata', repo, 'current')))) + + # For client 2 and 5, we are certain that Director metadata will have failed to # update. Image Repository metadata may or may not have updated before the # Director repository update failure, so we don't check that. Client 2 # started with root metadata for the Director repository, so that is all # we expect to find. - self.assertEqual( - ['root.' + tuf.conf.METADATA_FORMAT], - sorted(os.listdir(os.path.join(TEMP_CLIENT_DIRS[2], 'metadata', - 'director', 'current')))) + for instance_data in [TEST_INSTANCES[2], TEST_INSTANCES[5]]: + self.assertEqual( + ['root.' + tuf.conf.METADATA_FORMAT], + sorted(os.listdir(os.path.join(instance_data['client_dir'], + 'metadata', 'director', 'current')))) # Second: Check targets each Secondary client has been instructed to # install (and has in turn validated). - # Client 0 should have validated expected_updated_fileinfo. + # Client 0 should have validated fv_expected_updated_fileinfo. + self.assertEqual( + fv_expected_updated_fileinfo, + TEST_INSTANCES[0]['instance'].validated_targets_for_this_ecu[0]) + + # Client 3 should have validated pv_expected_updated_fileinfo. self.assertEqual( - expected_updated_fileinfo, - secondary_instances[0].validated_targets_for_this_ecu[0]) + pv_expected_updated_fileinfo, + TEST_INSTANCES[3]['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) + # Clients 1, 2, 4 and should have no validated targets. + self.assertFalse(TEST_INSTANCES[1]['instance'].validated_targets_for_this_ecu) + self.assertFalse(TEST_INSTANCES[2]['instance'].validated_targets_for_this_ecu) + self.assertFalse(TEST_INSTANCES[4]['instance'].validated_targets_for_this_ecu) + self.assertFalse(TEST_INSTANCES[5]['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') @@ -729,24 +821,54 @@ def test_40_process_metadata(self): def test_50_validate_image(self): - image_fname = 'TCU1.1.txt' + # In these tests, Secondary ECU were or were not given instructions to + # install a new update + # If instructed + # full verification Secondary will install 'TCU1.1.txt' + # partial verification Secondary will install 'BCU1.1.txt' + fv_image_fname = 'TCU1.1.txt' + pv_image_fname = 'BCU1.1.txt' + sample_image_location = os.path.join(demo.DEMO_DIR, 'images') - client_unverified_targets_dir = TEMP_CLIENT_DIRS[0] + '/unverified_targets' - if os.path.exists(client_unverified_targets_dir): - shutil.rmtree(client_unverified_targets_dir) - os.mkdir(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') - shutil.copy( - os.path.join(sample_image_location, image_fname), - client_unverified_targets_dir) + 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 + + shutil.copy( + os.path.join(sample_image_location, image_fname), + client_unverified_targets_dir) + + # Validate the appropriate update image for each secondary + + # Secondaries 0-2 are running full verification + TEST_INSTANCES[0]['instance'].validate_image(fv_image_fname) + + with self.assertRaises(uptane.Error): + TEST_INSTANCES[1]['instance'].validate_image(fv_image_fname) + with self.assertRaises(uptane.Error): + TEST_INSTANCES[2]['instance'].validate_image(fv_image_fname) - secondary_instances[0].validate_image(image_fname) + #Secondaries 3-5 are running partial verification + TEST_INSTANCES[3]['instance'].validate_image(pv_image_fname) with self.assertRaises(uptane.Error): - secondary_instances[1].validate_image(image_fname) + TEST_INSTANCES[4]['instance'].validate_image(pv_image_fname) with self.assertRaises(uptane.Error): - secondary_instances[2].validate_image(image_fname) + TEST_INSTANCES[5]['instance'].validate_image(pv_image_fname) diff --git a/uptane/clients/client.py b/uptane/clients/client.py new file mode 100644 index 0000000..a1fec4d --- /dev/null +++ b/uptane/clients/client.py @@ -0,0 +1,420 @@ +""" + + client.py + + + Provides common core functionality for Uptane clients: + - Primary and Secondary clients will inherit client class + and further implement additional functions as required + by the clients + +""" +from __future__ import print_function +from __future__ import unicode_literals + +import uptane # Import before TUF modules; may change tuf.conf values. + +import iso8601 + +import tuf.formats +import tuf.conf +import tuf.keys +import tuf.client.updater + +import uptane.formats +import uptane.common +import uptane.encoding.asn1_codec as asn1_codec + +from uptane.encoding.asn1_codec import DATATYPE_TIME_ATTESTATION + + +log = uptane.logging.getLogger('client') +log.addHandler(uptane.file_handler) +log.addHandler(uptane.console_handler) +log.setLevel(uptane.logging.DEBUG) + + +class Client(object): + + """ + + This class contains the necessary code to perform Uptane validation of + images and metadata which is required by both Primary and Secondary clients. + An implementation of Uptane should use code like this + to perform full validation of images and metadata. + + + + self.vin + A unique identifier for the vehicle that contains the ECU. + In this reference implementation, this conforms to + uptane.formats.VIN_SCHEMA. There is no need to use the vehicle's VIN in + particular; we simply need a unique identifier for the vehicle, known + to the Director. + + self.ecu_serial + A unique identifier for the ECU. In this reference implementation, + this conforms to uptane.formats.ECU_SERIAL_SCHEMA. + (In other implementations, the important point is that this should be + unique.) The Director should be aware of this identifier. + + self.ecu_key: + The signing key for the ECU. This key will be used to sign + Vehicle Manifests(by Primary ECU) or ECU Manifests(by Secondary ECU) + that will then be sent along to the Primary (and subsequently + to the Director). The Director should be aware of the corresponding + public key, so that it can validate these ECU Manifests. + Conforms to tuf.formats.ANYKEY_SCHEMA. + + self.updater: + A tuf.client.updater.Updater object used to retrieve metadata and + target files from the Director and Image repositories. + + self.full_client_dir: + The full path of the directory where all client data is stored for the + ECU. This includes verified and unverified metadata and images and + any temp files. Conforms to tuf.formats.PATH_SCHEMA. + + self.director_repo_name + The name of the Director repository (e.g. 'director'), as listed in the + map (or pinning) file (pinned.json). This value must appear in that file. + Used to distinguish between the Image Repository and the Director + Repository. Conforms to tuf.formats.REPOSITORY_NAME_SCHEMA. + + self.timeserver_public_key: + The public key of the Timeserver, which will be used to validate signed + time attestations from the Timeserver. + Conforms to tuf.formats.ANYKEY_SCHEMA. + + # TODO: Rename these two variables, valid -> verified, along with the + # verification functions. Do likewise in Secondary. + self.all_valid_timeserver_attestations: + A list of all attestations received from Timeservers that have been + verified by update_time(). + Items are appended to the end. + + self.all_valid_timeserver_times: + A list of all times extracted from all Timeserver attestations that have + been verified by update_time(). + Items are appended to the end. + + """ + + def __init__( + self, + full_client_dir, + director_repo_name, + vin, + ecu_serial, + ecu_key, + time, + timeserver_public_key): + """ + + Constructor for class Client + + + + full_client_dir See class docstring above. + + director_repo_name See class docstring above. + + vin See class docstring above. + + ecu_serial See class docstring above. + + primary_key See class docstring above. + + timeserver_public_key See class docstring above. + + time + An initial time to set the Primary's "clock" to, conforming to + tuf.formats.ISO8601_DATETIME_SCHEMA. + + + + + tuf.FormatError + if the arguments are not correctly formatted + + uptane.Error + if director_repo_name is not a known repository based on the + map/pinning file (pinned.json) + + + None. + """ + + # Check arguments: + tuf.formats.PATH_SCHEMA.check_match(full_client_dir) + tuf.formats.PATH_SCHEMA.check_match(director_repo_name) + uptane.formats.VIN_SCHEMA.check_match(vin) + uptane.formats.ECU_SERIAL_SCHEMA.check_match(ecu_serial) + tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(time) + tuf.formats.ANYKEY_SCHEMA.check_match(timeserver_public_key) + tuf.formats.ANYKEY_SCHEMA.check_match(ecu_key) + # TODO: Should also check that primary_key is a private key, not a + # public key. + + self.director_repo_name = director_repo_name + self.ecu_key = ecu_key + self.vin = vin + self.ecu_serial = ecu_serial + self.full_client_dir = full_client_dir + self.timeserver_public_key = timeserver_public_key + self.validated_targets = [] + # TODO: Consider removing time from [time] here and starting with an empty + # list, or setting time to 0 to start by default. + self.all_valid_timeserver_times = [time] + self.all_valid_timeserver_attestations = [] + + # Create a TAP-4-compliant updater object. This will read pinned.json + # and create single-repository updaters within it to handle connections to + # each repository. + self.updater = tuf.client.updater.Updater('updater') + + if director_repo_name not in self.updater.pinned_metadata['repositories']: + raise uptane.Error('Given name for the Director repository is not a ' + 'known repository, according to the pinned metadata from pinned.json') + + + + + + def refresh_toplevel_metadata(self): + """ + Refreshes client's metadata for the top-level roles: + root, targets, snapshot, and timestamp + + See tuf.client.updater.Updater.refresh() for details, or the + Uptane Standard, section 5.4.4.2 (Full Verification). + + This can raise TUF update exceptions like + - tuf.ExpiredMetadataError: + if after attempts to update the Root metadata succeeded or failed, + whatever currently trusted Root metadata we ended up with was expired. + - tuf.NoWorkingMirrorError: + if we could not obtain and verify all necessary metadata + """ + + # Refresh the Director first, per the Uptane Standard. + self.updater.refresh(repo_name=self.director_repo_name) + + # Now that we've dealt with the Director repository, deal with any and all + # other repositories, presumably Image Repositories. + for repository_name in self.updater.repositories: + if repository_name == self.director_repo_name: + continue + + self.updater.refresh(repo_name=repository_name) + + + + + + def get_validated_target_info(self, target_filepath): + """ + (Could be called: get Director's version of the fully validated target info) + + + + Returns trustworthy target information for the given target file + (specified by its file path), from the Director, validated against the + Image Repository (or whichever repositories are required per the + pinned.json file). + + The returned information has been cleared according to the trust + requirements of the pinning file (pinned.json) that this client is + equipped with. Assuming typical pinned.json configuration for Uptane, + this means that there is a multi-repository delegation to [the Director + Repository plus the Image Repository]. The target file info received + within this method is that from all repositories in the multi-repository + delegation, and each is guaranteed to be identical to the others in all + respects (e.g. crytographic hash and length) except for the "custom" + metadata field, since the Director includes an additional piece of + information in the fileinfo: the ECU Serial to which the target file is + assigned. + + This method returns only the Director's version of this target file info, + which includes that "custom" field with ECU Serial assignments. + + + Target file info compliant with tuf.formats.TARGETFILE_INFO_SCHEMA, + + + + + tuf.UnknownTargetError + if a given filepath is not listed by the consensus of Director and + Image Repository (or through whichever trusted path is specified by + this client's pinned.json file.) If info is returned, it will match + tuf.formats.TARGETFILE_SCHEMA and will have been validated by all + required parties. + + tuf.NoWorkingMirrorError + will be raised by the updater.target() call here if we are unable to + validate reliable target info for the target file specified (if the + repositories do not agree, or we could not reach them, or so on). + + uptane.Error + if the Director targets file has not provided information about the + given target_filepath, but target_filepath has nevertheless been + validated. This could happen if the map/pinning file for some reason + incorrectly set to not require metadata from the Director. + + """ + tuf.formats.RELPATH_SCHEMA.check_match(target_filepath) + + validated_target_info = self.updater.target( + target_filepath, multi_custom=True) + + # validated_target_info will now look something like this: + # { + # 'Director': { + # filepath: 'django/django.1.9.3.tgz', + # fileinfo: {hashes: ..., length: ..., custom: {'ecu_serial': 'ECU1010101'} } }, + # 'ImageRepo': { + # filepath: 'django/django.1.9.3.tgz', + # fileinfo: {hashes: ..., length: ... } } } + # } + + # We expect there to be an entry in the dict with key name equal to the + # name of the Director repository (specified in pinned.json). + + if self.director_repo_name not in validated_target_info: + # TODO: Consider a different exception class. This seems more like an + # assert statement, though.... If this happens, something is wrong in + # code, or pinned.json is misconfigured (to allow target validation + # whereby the Director is not specified in some multi-repository + # delegations) or the value of director_repo_name passed to the + # initialization of this object was wrong. Those are the edge cases I can + # come up with that could cause this. + + # If the Director repo specified as self.director_repo_name is not in + # pinned.json at all, we'd have thrown an error during __init__. If the + # repos couldn't provide validated target file info, we'd have caught an + # error earlier instead. + + raise uptane.Error('Unexpected behavior: did not receive target info from ' + 'Director repository (' + repr(self.director_repo_name) + ') for ' + 'a target (' + repr(target_filepath) + '). Is pinned.json configured ' + 'to allow some targets to validate without Director approval, or is ' + 'the wrong repository specified as the Director repository in the ' + 'initialization of this primary object?') + + # Defensive coding: this should already have been checked. + tuf.formats.TARGETFILE_SCHEMA.check_match( + validated_target_info[self.director_repo_name]) + + return validated_target_info[self.director_repo_name] + + + + + + def get_target_list_from_director(self): + """ + This method extracts the Director's instructions from the targets role in + the Director repository's metadata. These must still be validated against + the Image Repository in further calls. + """ + # TODO: This will have to be changed (along with the functions that depend + # on this function's output) once multi-role delegations can yield multiple + # targetfile_info objects. (Currently, we only yield more than one at the + # multi-repository delegation level.) + + # Refresh the top-level metadata first (all repositories). + log.debug('Refreshing top level metadata from all repositories.') + + # Refresh the top-level metadata first (all repositories). + self.refresh_toplevel_metadata() + + directed_targets = self.updater.targets_of_role( + rolename='targets', repo_name=self.director_repo_name) + + if not directed_targets: + log.info('A correctly signed statement from the Director indicates that ' + 'this vehicle has NO updates to install.') + else: + log.info('A correctly signed statement from the Director indicates that ' + 'this vehicle has updates to install:' + + repr([targ['filepath'] for targ in directed_targets])) + + return directed_targets + + + + + + def verify_timeserver_signature(self, timeserver_attestation): + """ + The response from the Timeserver should then be provided to this function. + This function attempts to verify the given attestation, + if timeserver_attestation is correctly signed by the expected Timeserver + + If the client is using ASN.1/DER metadata, then timeserver_attestation is + expected to be in that format, as a byte string. + Otherwise, we're using simple Python dictionaries and timeserver_attestation + conforms to uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA. + """ + # 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, DATATYPE_TIME_ATTESTATION) + + # Check format. + uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA.check_match( + timeserver_attestation) + + # Assume there's only one signature. This assumption is made for simplicity + # in this reference implementation. If the Timeserver needs to sign with + # multiple keys for some reason, that can be accommodated. + assert len(timeserver_attestation['signatures']) == 1 + + verified = uptane.common.verify_signature_over_metadata( + self.timeserver_public_key, + timeserver_attestation['signatures'][0], + timeserver_attestation['signed'], + DATATYPE_TIME_ATTESTATION) + + if not verified: + raise tuf.BadSignatureError('Timeserver returned an invalid signature. ' + 'Time is questionable, so not saved. If you see this persistently, ' + 'it is possible that there is a Man in the Middle attack underway.') + + # Return timeserver_attestation in JSON-compatible dictionary + return timeserver_attestation + + + + + + def update_verified_time(self, timeserver_attestation): + """ + This method extracts the time from provided timeserver_attestation, + which must be verified first, and updates the time of the client. + This method must only be called if timeserver_attestation is correctly + signed by the expected Timeserver key and is verified by the Primary + or Secondary for the list of nounces. + The new time will be used by this client (via TUF) in place of + system time when checking metadata for expiration. + """ + # Extract actual time from the timeserver's signed attestation. + new_timeserver_time = timeserver_attestation['signed']['time'] + + # Make sure the format is understandable to us before saving the + # attestation and time. Convert to a UNIX timestamp. + new_timeserver_time_unix = int(tuf.formats.datetime_to_unix_timestamp( + iso8601.parse_date(new_timeserver_time))) + tuf.formats.UNIX_TIMESTAMP_SCHEMA.check_match(new_timeserver_time_unix) + + # Save validated time. + self.all_valid_timeserver_times.append(new_timeserver_time) + + # Save the attestation itself as well, to provide to Secondaries (who need + # not trust us). + self.all_valid_timeserver_attestations.append(timeserver_attestation) + + # Set the client's clock. This will be used instead of system time by TUF. + tuf.conf.CLOCK_OVERRIDE = new_timeserver_time_unix diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index f26c271..c04aaa8 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -20,6 +20,8 @@ import uptane # Import before TUF modules; may change tuf.conf values. +from uptane.clients.client import Client + import os # For paths and makedirs import shutil # For copyfile import random # for nonces @@ -60,7 +62,7 @@ -class Primary(object): # Consider inheriting from Secondary and refactoring. +class Primary(Client): """ This class contains the necessary code to perform Uptane validation of @@ -84,16 +86,12 @@ class Primary(object): # Consider inheriting from Secondary and refactoring. (In other implementations, the important point is that this should be unique.) The Director should be aware of this identifier. - self.primary_key + self.ecu_key The signing key for this Primary ECU. This key will be used to sign Vehicle Manifests that will then be sent to the Director). The Director should be aware of the corresponding public key, so that it can validate these Vehicle Manifests. Conforms to tuf.formats.ANYKEY_SCHEMA. - self.updater - A tuf.client.updater.Updater object used to retrieve metadata and - target files from the Director and Supplier repositories. - self.full_client_dir The full path of the directory where all client data is stored for this Primary. This includes verified and unverified metadata and images and @@ -141,25 +139,13 @@ class Primary(object): # Consider inheriting from Secondary and refactoring. have already sent to the Timeserver. Will be checked against the Timeserver's response. - # TODO: Rename these two variables, valid -> verified, along with the - # verification functions. Do likewise in Secondary. - self.all_valid_timeserver_attestations: - A list of all attestations received from Timeservers that have been - verified by update_time(). - Items are appended to the end. - - self.all_valid_timeserver_times: - A list of all times extracted from all Timeserver attestations that have - been verified by update_time(). - Items are appended to the end. - self.distributable_full_metadata_archive_fname: The filename at which the full metadata archive is stored after each update cycle. Path is relative to uptane.WORKING_DIR. This is atomically moved into place (renamed) after it has been fully written, to avoid race conditions. - self.distributable_partial_metadata_fname: + self.distributable_partial_metadata_archive_fname: The filename at which the Director's targets metadata file is stored after each update cycle, once it is safe to use. This is atomically moved into place (renamed) after it has been fully written, to avoid race conditions. @@ -177,9 +163,8 @@ class Primary(object): # Consider inheriting from Secondary and refactoring. Lower-level methods called by primary_update_cycle() to perform retrieval and validation of metadata and data from central services: - refresh_toplevel_metadata() - get_target_list_from_director() - get_validated_target_info() + client->get_target_list_from_director() + client->get_validated_target_info() Components of the interface available to a Secondary client: register_ecu_manifest(vin, ecu_serial, nonce, signed_ecu_manifest) @@ -187,7 +172,7 @@ class Primary(object): # Consider inheriting from Secondary and refactoring. update_exists_for_ecu(ecu_serial) get_image_fname_for_ecu(ecu_serial) get_full_metadata_archive_fname() - get_partial_metadata_fname() + get_partial_metadata_archive_fname() register_new_secondary(ecu_serial) Private methods: @@ -198,6 +183,7 @@ class Primary(object): # Consider inheriting from Secondary and refactoring. import uptane.clients.primary as primary p = primary.Primary( full_client_dir='/Users/s/w/uptane/temp_primarymetadata', + director_repo_name='director' vin='vin11111', ecu_serial='ecu00000', timeserver_public_key=) @@ -227,7 +213,7 @@ def __init__( director_repo_name, # e.g. 'director'; value must appear in pinning file vin, # 'vin11111' ecu_serial, # 'ecu00000' - primary_key, + ecu_key, time, timeserver_public_key, my_secondaries=None): @@ -246,7 +232,7 @@ def __init__( ecu_serial See class docstring above. - primary_key See class docstring above. + ecu_key See class docstring above. timeserver_public_key See class docstring above. @@ -256,7 +242,6 @@ def __init__( An initial time to set the Primary's "clock" to, conforming to tuf.formats.ISO8601_DATETIME_SCHEMA. - tuf.FormatError @@ -277,23 +262,17 @@ def __init__( uptane.formats.VIN_SCHEMA.check_match(vin) uptane.formats.ECU_SERIAL_SCHEMA.check_match(ecu_serial) tuf.formats.ANYKEY_SCHEMA.check_match(timeserver_public_key) - tuf.formats.ANYKEY_SCHEMA.check_match(primary_key) - # TODO: Should also check that primary_key is a private key, not a + tuf.formats.ANYKEY_SCHEMA.check_match(ecu_key) + # TODO: Should also check that ecu_key is a private key, not a # public key. - self.vin = vin - self.ecu_serial = ecu_serial - self.full_client_dir = full_client_dir - # TODO: Consider removing time from [time] here and starting with an empty - # list, or setting time to 0 to start by default. - self.all_valid_timeserver_times = [time] - self.all_valid_timeserver_attestations = [] - self.timeserver_public_key = timeserver_public_key - self.primary_key = primary_key + super(Primary, self).__init__(full_client_dir, director_repo_name, vin, + ecu_serial, ecu_key, time, timeserver_public_key) + + self.my_secondaries = my_secondaries if self.my_secondaries is None: self.my_secondaries = [] # (because must not use mutable as default value) - self.director_repo_name = director_repo_name self.temp_full_metadata_archive_fname = os.path.join( full_client_dir, 'metadata', 'temp_full_metadata_archive.zip') @@ -301,12 +280,10 @@ def __init__( full_client_dir, 'metadata', 'full_metadata_archive.zip') # TODO: Some of these assumptions are unseemly. Reconsider. - self.temp_partial_metadata_fname = os.path.join( - full_client_dir, 'metadata', 'temp_director_targets.' + - tuf.conf.METADATA_FORMAT) - self.distributable_partial_metadata_fname = os.path.join( - full_client_dir, 'metadata', 'director_targets.' + - tuf.conf.METADATA_FORMAT) + self.temp_partial_metadata_archive_fname = os.path.join( + full_client_dir, 'metadata', 'temp_partial_metadata_archive.zip') + self.distributable_partial_metadata_archive_fname = os.path.join( + full_client_dir, 'metadata', 'partial_metadata_archive.zip') # Initializations not directly related to arguments. self.nonces_to_send = [] @@ -319,169 +296,6 @@ def __init__( self.ecu_manifests = {} - # Create a TUF-TAP-4-compliant updater object. This will read pinning.json - # and create single-repository updaters within it to handle connections to - # each repository. - self.updater = tuf.client.updater.Updater('updater') - - if director_repo_name not in self.updater.pinned_metadata['repositories']: - raise uptane.Error('Given name for the Director repository is not a ' - 'known repository, according to the pinned metadata from pinned.json') - - - - - - def refresh_toplevel_metadata(self): - """ - Refreshes client's metadata for the top-level roles: - root, targets, snapshot, and timestamp - - See tuf.client.updater.Updater.refresh() for details, or the - Uptane Standard, section 5.4.4.2 (Full Verification). - - # TODO: This function is duplicated in primary.py and secondary.py. It must - # be moved to a general client.py as part of a fix to issue #14 - # (github.com/uptane/uptane/issues/14). - This can raise TUF update exceptions like - - tuf.ExpiredMetadataError: - if after attempts to update the Root metadata succeeded or failed, - whatever currently trusted Root metadata we ended up with was expired. - - tuf.NoWorkingMirrorError: - if we could not obtain and verify all necessary metadata - """ - - # Refresh the Director first, per the Uptane Standard. - self.updater.refresh(repo_name=self.director_repo_name) - - # Now that we've dealt with the Director repository, deal with any and all - # other repositories, presumably Image Repositories. - for repository_name in self.updater.repositories: - if repository_name == self.director_repo_name: - continue - - self.updater.refresh(repo_name=repository_name) - - - - - - def get_target_list_from_director(self): - """ - This method extracts the Director's instructions from the targets role in - the Director repository's metadata. These must still be validated against - the Image Repository in further calls. - """ - # TODO: This will have to be changed (along with the functions that depend - # on this function's output) once multi-role delegations can yield multiple - # targetfile_info objects. (Currently, we only yield more than one at the - # multi-repository delegation level.) - directed_targets = self.updater.targets_of_role( - rolename='targets', repo_name=self.director_repo_name) - - return directed_targets - - - - - - def get_validated_target_info(self, target_filepath): - """ - (Could be called: get Director's version of the fully validated target info) - - - - Returns trustworthy target information for the given target file - (specified by its file path), from the Director, validated against the - Image Repository (or whichever repositories are required per the - pinned.json file). - - The returned information has been cleared according to the trust - requirements of the pinning file (pinned.json) that this client is - equipped with. Assuming typical pinned.json configuration for Uptane, - this means that there is a multi-repository delegation to [the Director - Repository plus the Image Repository]. The target file info received - within this method is that from all repositories in the multi-repository - delegation, and each is guaranteed to be identical to the others in all - respects (e.g. crytographic hash and length) except for the "custom" - metadata field, since the Director includes an additional piece of - information in the fileinfo: the ECU Serial to which the target file is - assigned. - - This method returns only the Director's version of this target file info, - which includes that "custom" field with ECU Serial assignments. - - - Target file info compliant with tuf.formats.TARGETFILE_INFO_SCHEMA, - - - - - tuf.UnknownTargetError - if a given filepath is not listed by the consensus of Director and - Image Repository (or through whichever trusted path is specified by - this client's pinned.json file.) If info is returned, it will match - tuf.formats.TARGETFILE_SCHEMA and will have been validated by all - required parties. - - tuf.NoWorkingMirrorError - will be raised by the updater.target() call here if we are unable to - validate reliable target info for the target file specified (if the - repositories do not agree, or we could not reach them, or so on). - - uptane.Error - if the Director targets file has not provided information about the - given target_filepath, but target_filepath has nevertheless been - validated. This could happen if the map/pinning file for some reason - incorrectly set to not require metadata from the Director. - - """ - tuf.formats.RELPATH_SCHEMA.check_match(target_filepath) - - validated_target_info = self.updater.target( - target_filepath, multi_custom=True) - - # validated_target_info will now look something like this: - # { - # 'Director': { - # filepath: 'django/django.1.9.3.tgz', - # fileinfo: {hashes: ..., length: ..., custom: {'ecu_serial': 'ECU1010101'} } }, - # 'ImageRepo': { - # filepath: 'django/django.1.9.3.tgz', - # fileinfo: {hashes: ..., length: ... } } } - # } - - # We expect there to be an entry in the dict with key name equal to the - # name of the Director repository (specified in pinned.json). - - if self.director_repo_name not in validated_target_info: - # TODO: Consider a different exception class. This seems more like an - # assert statement, though.... If this happens, something is wrong in - # code, or pinned.json is misconfigured (to allow target validation - # whereby the Director is not specified in some multi-repository - # delegations) or the value of director_repo_name passed to the - # initialization of this object was wrong. Those are the edge cases I can - # come up with that could cause this. - - # If the Director repo specified as self.director_repo_name is not in - # pinned.json at all, we'd have thrown an error during __init__. If the - # repos couldn't provide validated target file info, we'd have caught an - # error earlier instead. - - raise uptane.Error('Unexpected behavior: did not receive target info from' - ' Director repository (' + repr(self.director_repo_name) + ') for ' - 'a target (' + repr(target_filepath) + '). Is pinned.json configured ' - 'to allow some targets to validate without Director approval, or is' - 'the wrong repository specified as the Director repository in the ' - 'initialization of this primary object?') - - # Defensive coding: this should already have been checked. - tuf.formats.TARGETFILE_SCHEMA.check_match( - validated_target_info[self.director_repo_name]) - - return validated_target_info[self.director_repo_name] - - @@ -510,23 +324,10 @@ def primary_update_cycle(self): are deposited by TUF that does not have an extension that befits a file of type tuf.conf.METADATA_FORMAT. """ - log.debug('Refreshing top level metadata from all repositories.') - self.refresh_toplevel_metadata() - # Get the list of targets the director expects us to download and update to. - # Note that at this line, this target info is not yet validated with the - # Image Repository: that is done a few lines down. + # Get list of targets from the Director directed_targets = self.get_target_list_from_director() - if not directed_targets: - log.info('A correctly signed statement from the Director indicates that ' - 'this vehicle has NO updates to install.') - else: - log.info('A correctly signed statement from the Director indicates that ' - 'this vehicle has updates to install:' + - repr([targ['filepath'] for targ in directed_targets])) - - log.debug('Retrieving validated image file metadata from Image and ' 'Director Repositories.') @@ -770,7 +571,7 @@ def get_full_metadata_archive_fname(self): - def get_partial_metadata_fname(self): + def get_partial_metadata_archive_fname(self): """ Returns the absolute-path filename of the Director's targets.json metadata file, necessary for performing partial validation of target files (as a @@ -781,7 +582,7 @@ def get_partial_metadata_fname(self): file is completely written. If this Primary has never completed an update cycle, it will not exist yet. """ - return self.distributable_partial_metadata_fname + return self.distributable_partial_metadata_archive_fname @@ -895,13 +696,13 @@ def generate_signed_vehicle_manifest(self): # Convert to DER and sign, replacing the Python dictionary. signable_vehicle_manifest = asn1_codec.convert_signed_metadata_to_der( signable_vehicle_manifest, DATATYPE_VEHICLE_MANIFEST, - private_key=self.primary_key, resign=True) + private_key=self.ecu_key, resign=True) else: # If we're not using ASN.1, sign the Python dictionary in a JSON encoding. uptane.common.sign_signable( signable_vehicle_manifest, - [self.primary_key], + [self.ecu_key], DATATYPE_VEHICLE_MANIFEST) uptane.formats.SIGNABLE_VEHICLE_VERSION_MANIFEST_SCHEMA.check_match( @@ -1135,32 +936,9 @@ def update_time(self, timeserver_attestation): conforms to uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA. """ - # If we're using DER format, convert the attestation into something - # comprehensible instead. - if tuf.conf.METADATA_FORMAT == 'der': - timeserver_attestation = asn1_codec.convert_signed_der_to_dersigned_json( - timeserver_attestation, DATATYPE_TIME_ATTESTATION) - - # Check format. - uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA.check_match( - timeserver_attestation) - - - # Assume there's only one signature. This assumption is made for simplicity - # in this reference implementation. If the Timeserver needs to sign with - # multiple keys for some reason, that can be accomodated. - assert len(timeserver_attestation['signatures']) == 1 - - valid = uptane.common.verify_signature_over_metadata( - self.timeserver_public_key, - timeserver_attestation['signatures'][0], - timeserver_attestation['signed'], - DATATYPE_TIME_ATTESTATION) - - if not valid: - raise tuf.BadSignatureError('Timeserver returned an invalid signature. ' - 'Time is questionable, so not saved. If you see this persistently, ' - 'it is possible that there is a Man in the Middle attack underway.') + # Verify the signature of the timeserver on the attestation. If not verified, + # it raises a BadSignatureError + timeserver_attestation = self.verify_timeserver_signature(timeserver_attestation) for nonce in self.nonces_sent: if nonce not in timeserver_attestation['signed']['nonces']: @@ -1174,25 +952,8 @@ def update_time(self, timeserver_attestation): 'persistently, it is possible that there is a Man in the Middle ' 'attack underway.') - - # Extract actual time from the timeserver's signed attestation. - new_timeserver_time = timeserver_attestation['signed']['time'] - - # Make sure the format is understandable to us before saving the - # attestation and time. Convert to a UNIX timestamp. - new_timeserver_time_unix = int(tuf.formats.datetime_to_unix_timestamp( - iso8601.parse_date(new_timeserver_time))) - tuf.formats.UNIX_TIMESTAMP_SCHEMA.check_match(new_timeserver_time_unix) - - # Save validated time. - self.all_valid_timeserver_times.append(new_timeserver_time) - - # Save the attestation itself as well, to provide to Secondaries (who need - # not trust us). - self.all_valid_timeserver_attestations.append(timeserver_attestation) - - # Set the client's clock. This will be used instead of system time by TUF. - tuf.conf.CLOCK_OVERRIDE = new_timeserver_time_unix + # Update the time of Primary with the time in attestation + self.update_verified_time(timeserver_attestation) @@ -1207,7 +968,7 @@ def save_distributable_metadata_files(self): a zip archive of all the metadata files, from all repositories, validated by this Primary, for use by Full Verification Secondaries. - - self.distributable_partial_metadata_fname + - self.distributable_partial_metadata_archive_fname the Director Targets role file alone, for use by Partial Verification Secondaries @@ -1267,23 +1028,43 @@ def save_distributable_metadata_files(self): # Copy the Director's targets file to a temp location for partial-verifying # Secondaries. - director_targets_file = os.path.join( - self.full_client_dir, - 'metadata', - self.director_repo_name, - 'current', - 'targets.' + tuf.conf.METADATA_FORMAT) - if os.path.exists(self.temp_partial_metadata_fname): - os.remove(self.temp_partial_metadata_fname) - shutil.copyfile(director_targets_file, self.temp_partial_metadata_fname) + with zipfile.ZipFile(self.temp_partial_metadata_archive_fname, 'w') \ + as archive: + # Need 'target' metadata from only director repo + repo_name = self.director_repo_name + # Construct path to "current" metadata directory for that repository in + # the client metadata directory, relative to Uptane working directory. + abs_repo_dir = os.path.join(metadata_base_dir, repo_name, 'current') + + # Archive only 'targets' metadat file for partial verification + role_fname = 'targets.' + tuf.conf.METADATA_FORMAT + # Reconstruct file path relative to Uptane working directory. + role_abs_fname = os.path.join(abs_repo_dir, role_fname) + + # Make sure it's the right type of file. Should be a file, not a + # directory. Symlinks are OK. Should end in an extension matching + # tuf.conf.METADATA_FORMAT (presumably .json or .der, depending on + # that setting). + if not os.path.isfile(role_abs_fname) or not role_abs_fname.endswith( + '.' + tuf.conf.METADATA_FORMAT): + # Consider special error type. + raise uptane.Error('Unexpected file type in a metadata ' + 'directory: ' + repr(role_abs_fname) + ' Expecting only ' + + tuf.conf.METADATA_FORMAT + 'files.') + + # Write the file to the archive, adjusting the path in the archive so + # that when expanded, it resembles repository structure rather than + # a client directory structure. + archive.write(role_abs_fname, + os.path.join(repo_dir, 'metadata', role_fname)) # Now move both Full and Partial metadata files into place. For each file, # this happens atomically on POSIX-compliant systems and replaces any # existing file. os.rename( - self.temp_partial_metadata_fname, - self.distributable_partial_metadata_fname) + self.temp_partial_metadata_archive_fname, + self.distributable_partial_metadata_archive_fname) os.rename( self.temp_full_metadata_archive_fname, self.distributable_full_metadata_archive_fname) diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 88ea8fe..21ce956 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -24,15 +24,19 @@ import uptane # Import before TUF modules; may change tuf.conf values. +from uptane.clients.client import Client + import os # For paths and makedirs import shutil # For copyfile import random # for nonces import zipfile # to expand the metadata archive retrieved from the Primary import hashlib import iso8601 +import six import tuf.formats import tuf.keys +import tuf.keydb as key_database import tuf.client.updater import tuf.repository_tool as rt @@ -54,7 +58,7 @@ -class Secondary(object): +class Secondary(Client): """ @@ -84,10 +88,6 @@ class Secondary(object): corresponding public key, so that it can validate these ECU Manifests. Conforms to tuf.formats.ANYKEY_SCHEMA. - self.updater: - A tuf.client.updater.Updater object used to retrieve metadata and - target files from the Director and Image repositories. - self.full_client_dir: The full path of the directory where all client data is stored for this secondary. This includes verified and unverified metadata and images and @@ -161,7 +161,7 @@ class Secondary(object): process_metadata(metadata_archive_fname) _expand_metadata_archive(metadata_archive_fname) fully_validate_metadata() - get_validated_target_info(target_filepath) + client->get_validated_target_info(target_filepath) validate_image(image_fname) @@ -240,13 +240,10 @@ def __init__( if director_public_key is not None: tuf.formats.ANYKEY_SCHEMA.check_match(director_public_key) - self.director_repo_name = director_repo_name - self.ecu_key = ecu_key - self.vin = vin - self.ecu_serial = ecu_serial - self.full_client_dir = full_client_dir + super(Secondary, self).__init__(full_client_dir, director_repo_name, vin, + ecu_serial, ecu_key, time, timeserver_public_key) + self.director_proxy = None - self.timeserver_public_key = timeserver_public_key self.director_public_key = director_public_key self.partial_verifying = partial_verifying self.firmware_fileinfo = firmware_fileinfo @@ -262,16 +259,8 @@ def __init__( 'only the ') - # Create a TAP-4-compliant updater object. This will read pinned.json - # and create single-repository updaters within it to handle connections to - # each repository. - self.updater = tuf.client.updater.Updater('updater') - - if director_repo_name not in self.updater.pinned_metadata['repositories']: - raise uptane.Error('Given name for the Director repository is not a ' - 'known repository, according to the pinned metadata from pinned.json') - # We load the given time twice for simplicity in later code. + # TODO: Check if this is necessary. self.all_valid_timeserver_times = [time, time] self.last_nonce_sent = None @@ -400,30 +389,9 @@ def update_time(self, timeserver_attestation): If verification is successful, switch to a new nonce for next time. """ - # 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, DATATYPE_TIME_ATTESTATION) - - # Check format. - uptane.formats.SIGNABLE_TIMESERVER_ATTESTATION_SCHEMA.check_match( - timeserver_attestation) - - # Assume there's only one signature. - assert len(timeserver_attestation['signatures']) == 1 - - verified = uptane.common.verify_signature_over_metadata( - self.timeserver_public_key, - timeserver_attestation['signatures'][0], - timeserver_attestation['signed'], - DATATYPE_TIME_ATTESTATION) - - if not verified: - raise tuf.BadSignatureError('Timeserver returned an invalid signature. ' - 'Time is questionable, so not saved. If you see this persistently, ' - 'it is possible that there is a Man in the Middle attack underway.') - + # Verify the signature of the timeserver on the attestation. If not verified, + # it raises a BadSignatureError + timeserver_attestation = self.verify_timeserver_signature(timeserver_attestation) # If the most recent nonce we sent is not in the timeserver attestation, # then we don't trust the timeserver attestation. @@ -447,58 +415,9 @@ def update_time(self, timeserver_attestation): 'underway between the vehicle and the servers, or within the ' 'vehicle.') - # Extract actual time from the timeserver's signed attestation. - new_timeserver_time = timeserver_attestation['signed']['time'] - - # Make sure the format is understandable to us before saving the - # time. Convert to a UNIX timestamp. - new_timeserver_time_unix = int(tuf.formats.datetime_to_unix_timestamp( - iso8601.parse_date(new_timeserver_time))) - tuf.formats.UNIX_TIMESTAMP_SCHEMA.check_match(new_timeserver_time_unix) - - # Save verified time. - self.all_valid_timeserver_times.append(new_timeserver_time) - - # Set the client's clock. This will be used instead of system time by TUF. - tuf.conf.CLOCK_OVERRIDE = new_timeserver_time_unix + # Update the time of Secondary with the time in attestation + self.update_verified_time(timeserver_attestation) - # Use a new nonce next time, since the nonce we were using has now been - # used to successfully verify a timeserver attestation. - self.change_nonce() - - - - - - def refresh_toplevel_metadata(self): - """ - Refreshes client's metadata for the top-level roles: - root, targets, snapshot, and timestamp - - See tuf.client.updater.Updater.refresh() for details, or the - Uptane Standard, section 5.4.4.2 (Full Verification). - - # TODO: This function is duplicated in primary.py and secondary.py. It must - # be moved to a general client.py as part of a fix to issue #14 - # (github.com/uptane/uptane/issues/14). - This can raise TUF update exceptions like - - tuf.ExpiredMetadataError: - if after attempts to update the Root metadata succeeded or failed, - whatever currently trusted Root metadata we ended up with was expired. - - tuf.NoWorkingMirrorError: - if we could not obtain and verify all necessary metadata - """ - - # Refresh the Director first, per the Uptane Standard. - self.updater.refresh(repo_name=self.director_repo_name) - - # Now that we've dealt with the Director repository, deal with any and all - # other repositories, presumably Image Repositories. - for repository_name in self.updater.repositories: - if repository_name == self.director_repo_name: - continue - - self.updater.refresh(repo_name=repository_name) @@ -537,16 +456,14 @@ def fully_validate_metadata(self): """ - # Refresh the top-level metadata first (all repositories). - self.refresh_toplevel_metadata() + # Get list of targets from the Director + directed_targets = self.get_target_list_from_director() validated_targets_for_this_ecu = [] # Comb through the Director's direct instructions, picking out only the # target(s) earmarked for this ECU (by ECU Serial) - for target in self.updater.targets_of_role( - rolename='targets', repo_name=self.director_repo_name): - + for target in directed_targets: # Ignore target info not marked as being for this ECU. if 'custom' not in target['fileinfo'] or \ 'ecu_serial' not in target['fileinfo']['custom'] or \ @@ -564,37 +481,117 @@ 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 - def get_validated_target_info(self, target_filepath): + def partial_validate_metadata(self): """ - COPIED EXACTLY, MINUS COMMENTS, from primary.py. - # TODO: Refactor later. - Throws tuf.UnknownTargetError if unable to find/validate a target. + + 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 + + + + 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, 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 + 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(target_filepath) - validated_target_info = self.updater.target( - target_filepath, multi_custom=True) - if self.director_repo_name not in validated_target_info: + validated_targets_for_this_ecu = [] - raise tuf.Error('Unexpected behavior: did not receive target info from ' - 'Director repository (' + repr(self.director_repo_name) + ') for ' - 'a target (' + repr(target_filepath) + '). Is pinned.json configured ' - 'to allow some targets to validate without Director approval, or is' - 'the wrong repository specified as the Director repository in the ' - 'initialization of this primary object?') + upperbound_filelength = tuf.conf.DEFAULT_TARGETS_REQUIRED_LENGTH - tuf.formats.TARGETFILE_SCHEMA.check_match( - validated_target_info[self.director_repo_name]) + # Add director key in the key_database for metadata verification + try: + key_database.add_key( + self.director_public_key, repository_name=self.director_repo_name) + except tuf.KeyAlreadyExistsError: + log.debug('Key already present in the key database') - return validated_target_info[self.director_repo_name] + director_obj = self.updater.repositories['director'] + + # _update_metadata carries out the verification of metadata + # and then updates the metadata of the repository + director_obj._update_metadata('targets', upperbound_filelength) + + # TODO: If this Targets metadata file indicates that + # the Timeserver key should be rotated then reset the + # clock used to determine the expiration of metadata + # to a minimal value. It will be updated in the next cycle. + + # Is the metadata is not expired? + director_obj._ensure_not_expired(director_obj.metadata['current']['targets'], 'targets') + + validated_targets_from_director = [] + # Do we have metadata for 'targets'? + if 'targets' not in director_obj.metadata['current']: + log.debug('No metadata for \'targtes\'. Unable to determine targets.') + validated_targets_from_director = [] + + # Get the targets specified by the role itself. + for filepath, fileinfo in six.iteritems(director_obj.metadata['current']['targets']['targets']): + new_target = {} + new_target['filepath'] = filepath + new_target['fileinfo'] = fileinfo + + validated_targets_from_director.append(new_target) + + for target in validated_targets_from_director: + # Ignore target info not marked as being for this ECU. + if 'custom' not in target['fileinfo'] or \ + 'ecu_serial' not in target['fileinfo']['custom'] or \ + self.ecu_serial != target['fileinfo']['custom']['ecu_serial']: + continue + + validated_targets_for_this_ecu.append(target) + + if validated_targets_from_director: + self.validated_targets_for_this_ecu = validated_targets_for_this_ecu @@ -602,18 +599,56 @@ 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 + tuf.BadSignatureError. + (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) self._expand_metadata_archive(metadata_archive_fname) - # This entails using the local metadata files as a repository. - self.fully_validate_metadata() + # This verification entails using the local metadata files as a repository. + if self.partial_verifying: + self.partial_validate_metadata() + else: + self.fully_validate_metadata()