1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Unit tests for pw_software_update/dev_sign.py.""" 15 16from dataclasses import dataclass 17from pathlib import Path 18import tempfile 19from typing import NamedTuple 20import unittest 21 22from pw_software_update import dev_sign, root_metadata, update_bundle 23from pw_software_update.verify import VerificationError, verify_bundle 24from pw_software_update.tuf_pb2 import SignedRootMetadata 25from pw_software_update.update_bundle_pb2 import UpdateBundle 26 27 28def gen_unsigned_bundle( 29 signed_root_metadata: SignedRootMetadata | None = None, 30 targets_metadata_version: int = 0, 31) -> UpdateBundle: 32 """Generates an unsigned test bundle.""" 33 with tempfile.TemporaryDirectory() as tempdir_name: 34 targets_root = Path(tempdir_name) 35 foo_path = targets_root / 'foo.bin' 36 bar_path = targets_root / 'bar.bin' 37 baz_path = targets_root / 'baz.bin' 38 qux_path = targets_root / 'subdir' / 'qux.exe' 39 foo_bytes = b'\xf0\x0b\xa4' 40 bar_bytes = b'\x0b\xa4\x99' 41 baz_bytes = b'\xba\x59\x06' 42 qux_bytes = b'\x8a\xf3\x12' 43 foo_path.write_bytes(foo_bytes) 44 bar_path.write_bytes(bar_bytes) 45 baz_path.write_bytes(baz_bytes) 46 (targets_root / 'subdir').mkdir() 47 qux_path.write_bytes(qux_bytes) 48 targets = { 49 foo_path: 'foo', 50 bar_path: 'bar', 51 baz_path: 'baz', 52 qux_path: 'qux', 53 } 54 return update_bundle.gen_unsigned_update_bundle( 55 targets, 56 root_metadata=signed_root_metadata, 57 targets_metadata_version=targets_metadata_version, 58 ) 59 60 61class TestKey(NamedTuple): 62 """A test key pair""" 63 64 public: bytes 65 private: bytes 66 67 68@dataclass 69class BundleOptions: 70 """Parameters used in test bundle generations.""" 71 72 root_key_version: int = 0 73 root_metadata_version: int = 0 74 targets_key_version: int = 0 75 targets_metadata_version: int = 0 76 77 78def gen_signed_bundle(options: BundleOptions) -> UpdateBundle: 79 """Generates a test bundle per given options.""" 80 # Root keys look up table: version->TestKey 81 root_keys = { 82 0: TestKey( 83 private=( 84 b'-----BEGIN PRIVATE KEY-----\n' 85 b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl' 86 b'346MS5N/quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6' 87 b'HtgXs/7YSsvPMWZz9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9a' 88 b'M6QKLCNrCOqYjw6t' 89 b'\n-----END PRIVATE KEY-----\n' 90 ), 91 public=( 92 b'-----BEGIN PUBLIC KEY-----\n' 93 b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2' 94 b'ErLzzFmc/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjaw' 95 b'jqmI8OrQ==' 96 b'\n-----END PUBLIC KEY-----\n' 97 ), 98 ), 99 1: TestKey( 100 private=( 101 b'-----BEGIN PRIVATE KEY-----\n' 102 b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE3MRbMxo' 103 b'Gv3I/Ok/0qE8GV/mQuIbZo9kk+AsJnYetQ6hRANCAAQ5UhycwdcfYe34' 104 b'NpmG32t0klnKlrUbk3LyvYLq5uDWG2MfP3L0ciNFsEnW7vHpqqjKsoru' 105 b'Qt30G10K7D+reC77' 106 b'\n-----END PRIVATE KEY-----\n' 107 ), 108 public=( 109 b'-----BEGIN PUBLIC KEY-----\n' 110 b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOVIcnMHXH2Ht+DaZht9rd' 111 b'JJZypa1G5Ny8r2C6ubg1htjHz9y9HIjRbBJ1u7x6aqoyrKK7kLd9BtdCu' 112 b'w/q3gu+w==' 113 b'\n-----END PUBLIC KEY-----\n' 114 ), 115 ), 116 } 117 118 # Targets keys look up table: version->TestKey 119 targets_keys = { 120 0: TestKey( 121 private=( 122 b'-----BEGIN PRIVATE KEY-----\n' 123 b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84' 124 b'HzC51nhhf2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbA' 125 b'uOjy2NuusFOUpeC6TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe70' 126 b'6PUMbWLpyQ10ThVM' 127 b'\n-----END PRIVATE KEY-----\n' 128 ), 129 public=( 130 b'-----BEGIN PUBLIC KEY-----\n' 131 b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbr' 132 b'rBTlKXgukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6c' 133 b'kNdE4VTA==' 134 b'\n-----END PUBLIC KEY-----\n' 135 ), 136 ), 137 1: TestKey( 138 private=( 139 b'-----BEGIN PRIVATE KEY-----\n' 140 b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Q+u2KoO' 141 b'CwpY1HEKDTIjQXmTlxhoo3gVkE7nrtHhMemhRANCAASgc+0AHCfUxoHy' 142 b'+ZkSslLvMufiDqGPABvfuKzHd0wUWs2Y0eIvQc7tsBP0bcuJsFuxvL6a' 143 b'8Ek7y3kUmFWVL01v' 144 b'\n-----END PRIVATE KEY-----\n' 145 ), 146 public=( 147 b'-----BEGIN PUBLIC KEY-----\n' 148 b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHPtABwn1MaB8vmZErJS7' 149 b'zLn4g6hjwAb37isx3dMFFrNmNHiL0HO7bAT9G3LibBbsby+mvBJO8t5FJ' 150 b'hVlS9Nbw==' 151 b'\n-----END PUBLIC KEY-----\n' 152 ), 153 ), 154 } 155 156 unsigned_root = root_metadata.gen_root_metadata( 157 root_metadata.RootKeys([root_keys[options.root_key_version].public]), 158 root_metadata.TargetsKeys( 159 [targets_keys[options.targets_key_version].public] 160 ), 161 version=options.root_metadata_version, 162 ) 163 164 serialized_root = unsigned_root.SerializeToString() 165 signed_root = SignedRootMetadata(serialized_root_metadata=serialized_root) 166 signed_root = dev_sign.sign_root_metadata( 167 signed_root, root_keys[options.root_key_version].private 168 ) 169 # Additionaly sign the root metadata with the previous version of root key 170 # to enable upgrading from the previous root. 171 if options.root_key_version > 0: 172 signed_root = dev_sign.sign_root_metadata( 173 signed_root, root_keys[options.root_key_version - 1].private 174 ) 175 176 unsigned_bundle = gen_unsigned_bundle( 177 signed_root_metadata=signed_root, 178 targets_metadata_version=options.targets_metadata_version, 179 ) 180 signed_bundle = dev_sign.sign_update_bundle( 181 unsigned_bundle, targets_keys[options.targets_key_version].private 182 ) 183 184 return signed_bundle 185 186 187class VerifyBundleTest(unittest.TestCase): 188 """Bundle verification test cases.""" 189 190 def test_self_verification(self): # pylint: disable=no-self-use 191 incoming = gen_signed_bundle(BundleOptions()) 192 verify_bundle(incoming, trusted=incoming) 193 194 def test_root_key_rotation(self): # pylint: disable=no-self-use 195 trusted = gen_signed_bundle(BundleOptions(root_key_version=0)) 196 incoming = gen_signed_bundle(BundleOptions(root_key_version=1)) 197 verify_bundle(incoming, trusted) 198 199 def test_root_metadata_anti_rollback(self): 200 trusted = gen_signed_bundle(BundleOptions(root_metadata_version=1)) 201 incoming = gen_signed_bundle(BundleOptions(root_metadata_version=0)) 202 with self.assertRaises(VerificationError): 203 verify_bundle(incoming, trusted) 204 205 def test_root_metadata_anti_rollback_with_key_rotation(self): 206 trusted = gen_signed_bundle( 207 BundleOptions(root_key_version=0, root_metadata_version=1) 208 ) 209 incoming = gen_signed_bundle( 210 BundleOptions(root_key_version=1, root_metadata_version=0) 211 ) 212 # Anti-rollback enforced regardless of key rotation. 213 with self.assertRaises(VerificationError): 214 verify_bundle(incoming, trusted) 215 216 def test_missing_root(self): 217 incoming = gen_signed_bundle(BundleOptions()) 218 incoming.ClearField('root_metadata') 219 with self.assertRaises(VerificationError): 220 verify_bundle(incoming, trusted=incoming) 221 222 def test_targets_key_rotation(self): # pylint: disable=no-self-use 223 trusted = gen_signed_bundle(BundleOptions(targets_key_version=0)) 224 incoming = gen_signed_bundle(BundleOptions(targets_key_version=1)) 225 verify_bundle(incoming, trusted) 226 227 def test_targets_metadata_anti_rollback(self): 228 trusted = gen_signed_bundle(BundleOptions(targets_metadata_version=1)) 229 incoming = gen_signed_bundle(BundleOptions(targets_metadata_version=0)) 230 with self.assertRaises(VerificationError): 231 verify_bundle(incoming, trusted) 232 233 def test_targets_fastforward_recovery(self): # pylint: disable=no-self-use 234 trusted = gen_signed_bundle( 235 BundleOptions(targets_key_version=0, targets_metadata_version=999) 236 ) 237 # Revoke key and bring back the metadata version. 238 incoming = gen_signed_bundle( 239 BundleOptions(targets_key_version=1, targets_metadata_version=0) 240 ) 241 # Anti-rollback is not enforced upon key rotation. 242 verify_bundle(incoming, trusted) 243 244 245if __name__ == '__main__': 246 unittest.main() 247