| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Unit tests for pw_software_update/dev_sign.py.""" |
| |
| from dataclasses import dataclass |
| from pathlib import Path |
| import tempfile |
| from typing import NamedTuple |
| import unittest |
| |
| from pw_software_update import dev_sign, root_metadata, update_bundle |
| from pw_software_update.verify import VerificationError, verify_bundle |
| from pw_software_update.tuf_pb2 import SignedRootMetadata |
| from pw_software_update.update_bundle_pb2 import UpdateBundle |
| |
| |
| def gen_unsigned_bundle(signed_root_metadata: SignedRootMetadata = None, |
| targets_metadata_version: int = 0) -> UpdateBundle: |
| """Generates an unsigned test bundle.""" |
| with tempfile.TemporaryDirectory() as tempdir_name: |
| targets_root = Path(tempdir_name) |
| foo_path = targets_root / 'foo.bin' |
| bar_path = targets_root / 'bar.bin' |
| baz_path = targets_root / 'baz.bin' |
| qux_path = targets_root / 'subdir' / 'qux.exe' |
| foo_bytes = b'\xf0\x0b\xa4' |
| bar_bytes = b'\x0b\xa4\x99' |
| baz_bytes = b'\xba\x59\x06' |
| qux_bytes = b'\x8a\xf3\x12' |
| foo_path.write_bytes(foo_bytes) |
| bar_path.write_bytes(bar_bytes) |
| baz_path.write_bytes(baz_bytes) |
| (targets_root / 'subdir').mkdir() |
| qux_path.write_bytes(qux_bytes) |
| targets = { |
| foo_path: 'foo', |
| bar_path: 'bar', |
| baz_path: 'baz', |
| qux_path: 'qux', |
| } |
| return update_bundle.gen_unsigned_update_bundle( |
| targets, |
| root_metadata=signed_root_metadata, |
| targets_metadata_version=targets_metadata_version) |
| |
| |
| class TestKey(NamedTuple): |
| """A test key pair""" |
| public: bytes |
| private: bytes |
| |
| |
| @dataclass |
| class BundleOptions: |
| """Parameters used in test bundle generations.""" |
| root_key_version: int = 0 |
| root_metadata_version: int = 0 |
| targets_key_version: int = 0 |
| targets_metadata_version: int = 0 |
| |
| |
| def gen_signed_bundle(options: BundleOptions) -> UpdateBundle: |
| """Generates a test bundle per given options.""" |
| # Root keys look up table: version->TestKey |
| root_keys = { |
| 0: |
| TestKey( |
| private=( |
| b'-----BEGIN PRIVATE KEY-----\n' |
| b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl' |
| b'346MS5N/quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6' |
| b'HtgXs/7YSsvPMWZz9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9a' |
| b'M6QKLCNrCOqYjw6t' |
| b'\n-----END PRIVATE KEY-----\n'), |
| public=( |
| b'-----BEGIN PUBLIC KEY-----\n' |
| b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2' |
| b'ErLzzFmc/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjaw' |
| b'jqmI8OrQ==' |
| b'\n-----END PUBLIC KEY-----\n')), |
| 1: |
| TestKey( |
| private=( |
| b'-----BEGIN PRIVATE KEY-----\n' |
| b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE3MRbMxo' |
| b'Gv3I/Ok/0qE8GV/mQuIbZo9kk+AsJnYetQ6hRANCAAQ5UhycwdcfYe34' |
| b'NpmG32t0klnKlrUbk3LyvYLq5uDWG2MfP3L0ciNFsEnW7vHpqqjKsoru' |
| b'Qt30G10K7D+reC77' |
| b'\n-----END PRIVATE KEY-----\n'), |
| public=( |
| b'-----BEGIN PUBLIC KEY-----\n' |
| b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOVIcnMHXH2Ht+DaZht9rd' |
| b'JJZypa1G5Ny8r2C6ubg1htjHz9y9HIjRbBJ1u7x6aqoyrKK7kLd9BtdCu' |
| b'w/q3gu+w==' |
| b'\n-----END PUBLIC KEY-----\n')) |
| } |
| |
| # Targets keys look up table: version->TestKey |
| targets_keys = { |
| 0: |
| TestKey( |
| private=( |
| b'-----BEGIN PRIVATE KEY-----\n' |
| b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84' |
| b'HzC51nhhf2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbA' |
| b'uOjy2NuusFOUpeC6TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe70' |
| b'6PUMbWLpyQ10ThVM' |
| b'\n-----END PRIVATE KEY-----\n'), |
| public=( |
| b'-----BEGIN PUBLIC KEY-----\n' |
| b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbr' |
| b'rBTlKXgukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6c' |
| b'kNdE4VTA==' |
| b'\n-----END PUBLIC KEY-----\n')), |
| 1: |
| TestKey( |
| private=( |
| b'-----BEGIN PRIVATE KEY-----\n' |
| b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Q+u2KoO' |
| b'CwpY1HEKDTIjQXmTlxhoo3gVkE7nrtHhMemhRANCAASgc+0AHCfUxoHy' |
| b'+ZkSslLvMufiDqGPABvfuKzHd0wUWs2Y0eIvQc7tsBP0bcuJsFuxvL6a' |
| b'8Ek7y3kUmFWVL01v' |
| b'\n-----END PRIVATE KEY-----\n'), |
| public=( |
| b'-----BEGIN PUBLIC KEY-----\n' |
| b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHPtABwn1MaB8vmZErJS7' |
| b'zLn4g6hjwAb37isx3dMFFrNmNHiL0HO7bAT9G3LibBbsby+mvBJO8t5FJ' |
| b'hVlS9Nbw==' |
| b'\n-----END PUBLIC KEY-----\n')) |
| } |
| |
| unsigned_root = root_metadata.gen_root_metadata( |
| root_metadata.RootKeys([root_keys[options.root_key_version].public]), |
| root_metadata.TargetsKeys( |
| [targets_keys[options.targets_key_version].public]), |
| version=options.root_metadata_version) |
| |
| serialized_root = unsigned_root.SerializeToString() |
| signed_root = SignedRootMetadata(serialized_root_metadata=serialized_root) |
| signed_root = dev_sign.sign_root_metadata( |
| signed_root, root_keys[options.root_key_version].private) |
| # Additionaly sign the root metadata with the previous version of root key |
| # to enable upgrading from the previous root. |
| if options.root_key_version > 0: |
| signed_root = dev_sign.sign_root_metadata( |
| signed_root, root_keys[options.root_key_version - 1].private) |
| |
| unsigned_bundle = gen_unsigned_bundle( |
| signed_root_metadata=signed_root, |
| targets_metadata_version=options.targets_metadata_version) |
| signed_bundle = dev_sign.sign_update_bundle( |
| unsigned_bundle, targets_keys[options.targets_key_version].private) |
| |
| return signed_bundle |
| |
| |
| class VerifyBundleTest(unittest.TestCase): |
| """Bundle verification test cases.""" |
| def test_self_verification(self): # pylint: disable=no-self-use |
| incoming = gen_signed_bundle(BundleOptions()) |
| verify_bundle(incoming, trusted=incoming) |
| |
| def test_root_key_rotation(self): # pylint: disable=no-self-use |
| trusted = gen_signed_bundle(BundleOptions(root_key_version=0)) |
| incoming = gen_signed_bundle(BundleOptions(root_key_version=1)) |
| verify_bundle(incoming, trusted) |
| |
| def test_root_metadata_anti_rollback(self): |
| trusted = gen_signed_bundle(BundleOptions(root_metadata_version=1)) |
| incoming = gen_signed_bundle(BundleOptions(root_metadata_version=0)) |
| with self.assertRaises(VerificationError): |
| verify_bundle(incoming, trusted) |
| |
| def test_root_metadata_anti_rollback_with_key_rotation(self): |
| trusted = gen_signed_bundle( |
| BundleOptions(root_key_version=0, root_metadata_version=1)) |
| incoming = gen_signed_bundle( |
| BundleOptions(root_key_version=1, root_metadata_version=0)) |
| # Anti-rollback enforced regardless of key rotation. |
| with self.assertRaises(VerificationError): |
| verify_bundle(incoming, trusted) |
| |
| def test_missing_root(self): |
| incoming = gen_signed_bundle(BundleOptions()) |
| incoming.ClearField('root_metadata') |
| with self.assertRaises(VerificationError): |
| verify_bundle(incoming, trusted=incoming) |
| |
| def test_targets_key_rotation(self): # pylint: disable=no-self-use |
| trusted = gen_signed_bundle(BundleOptions(targets_key_version=0)) |
| incoming = gen_signed_bundle(BundleOptions(targets_key_version=1)) |
| verify_bundle(incoming, trusted) |
| |
| def test_targets_metadata_anti_rollback(self): |
| trusted = gen_signed_bundle(BundleOptions(targets_metadata_version=1)) |
| incoming = gen_signed_bundle(BundleOptions(targets_metadata_version=0)) |
| with self.assertRaises(VerificationError): |
| verify_bundle(incoming, trusted) |
| |
| def test_targets_fastforward_recovery(self): # pylint: disable=no-self-use |
| trusted = gen_signed_bundle( |
| BundleOptions(targets_key_version=0, targets_metadata_version=999)) |
| # Revoke key and bring back the metadata version. |
| incoming = gen_signed_bundle( |
| BundleOptions(targets_key_version=1, targets_metadata_version=0)) |
| # Anti-rollback is not enforced upon key rotation. |
| verify_bundle(incoming, trusted) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |