1# Copyright 2017 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import logging 6import os 7 8from autotest_lib.client.common_lib import error 9from autotest_lib.client.common_lib.cros import cr50_utils 10from autotest_lib.server.cros.faft.cr50_test import Cr50Test 11 12 13class firmware_Cr50BID(Cr50Test): 14 """Verify cr50 board id behavior on a board id locked image. 15 16 Check that cr50 will not accept mismatched board ids when it is running a 17 board id locked image. 18 19 Set the board id on a non board id locked image and verify cr50 will 20 rollback when it is updated to a mismatched board id image. 21 22 Release images can be tested by passing in the release version and board 23 id. The images on google storage have this in the filename. Use those same 24 values for the test. 25 26 If no board id or release version is given, the test will download the 27 prebuilt debug image from google storage. It has the board id 28 TEST:0xffff:0xff00. If you need to add another device to the lab or want to 29 test locally, you can add these values to the manifest to sign the image. 30 "board_id": 0x54455354, 31 "board_id_mask": 0xffff, 32 "board_id_flags": 0xff00, 33 34 You can also use the following command to create the image. 35 CR50_BOARD_ID='TEST:ffff:ff00' util/signer/bs 36 37 If you want to use something other than the test board id info, you have to 38 input the release version and board id. 39 40 @param dev_path: path to the node locked dev image. 41 @param bid_path: local path for the board id locked image. The other bid 42 args will be ignored, and the board id info will be gotten 43 from the file. 44 @param release_ver: The rw version and image board id. Needed if you want to 45 test a released board id locked image. 46 """ 47 version = 1 48 49 MAX_BID = 0xffffffff 50 51 # The universal image can be run on any system no matter the board id. 52 UNIVERSAL = 'universal' 53 # The board id locked can only run on devices with the right chip board id. 54 BID_LOCKED = 'board_id_locked' 55 # BID support was added in 0.0.21. Support for keeping the rollback state 56 # after AP boot was added in 0.3.4. Any version after 0.3.4 should be ok to 57 # use to detect rollback. Use 0.3.9 to get more bug fixes. 58 BID_SUPPORT = '0.3.9' 59 60 # Board id locked debug files will use the board id, mask, and flags in the 61 # gs filename 62 TEST_BOARD_ID = 'TEST' 63 TEST_MASK = 0xffff 64 TEST_FLAGS = 0xff00 65 TEST_IMAGE_BID_INFO = [TEST_BOARD_ID, TEST_MASK, TEST_FLAGS] 66 BID_MISMATCH = ['Board ID mismatched, but can not reboot.'] 67 BID_ERROR = 5 68 SUCCESS = 0 69 70 # BID_BASE_TESTS is a list with the the board id and flags to test for each 71 # run. Each item in the list is a list of [board_id, flags, exit status]. 72 # exit_status should be BID_ERROR if the board id and flags should not be 73 # compatible with the board id locked image. 74 # 75 # A image without board id will be able to run on a device with all of the 76 # board id and flag combinations. 77 # 78 # When using a non-symbolic board id, make sure the length of the string is 79 # greater than 4. If the string length is less than 4, usb_updater will 80 # treat it as a symbolic string 81 # ex: bid of 0 needs to be given as '0x0000'. If it were given as '0', the 82 # board id value would be interpreted as ord('0') 83 # 84 # These base tests are be true no matter the board id, mask, or flags. If a 85 # value is None, then it will be replaced with the test board id or flags 86 # while running the test. 87 BID_BASE_TESTS = [ 88 [None, None, SUCCESS], 89 90 # All 1s in the board id flags should be acceptable no matter the 91 # actual image flags 92 [None, MAX_BID, SUCCESS], 93 ] 94 95 # Settings to test all of the cr50 BID responses. The dictionary conatins 96 # the name of the BID verification as the key and a list as a value. 97 # 98 # The value of the list is the image to start running the test with then 99 # the method to update to the board id locked image as the value. 100 # 101 # If the start image is 'board_id_locked', we won't try to update to the 102 # board id locked image. 103 BID_TEST_TYPE = [ 104 # Verify that the board id locked image rejects invalid board ids 105 ['get/set', BID_LOCKED], 106 107 # Verify the cr50 response when doing a normal update to a board id 108 # locked image. If there is a board id mismatch, cr50 should rollback 109 # to the image that was already running. 110 ['rollback', UNIVERSAL], 111 112 # TODO (mruthven): add support for verifying recovery 113 # Certain devices are not able to successfully jump to the recovery 114 # image when the TPM is locked down. We need to find a way to verify the 115 # DUT is in recovery without being able to ssh into the DUT. 116 ] 117 118 def initialize(self, host, cmdline_args, dev_path='', bid_path='', 119 release_ver=None, test_subset=None, full_args={}): 120 # Restore the original image, rlz code, and board id during cleanup. 121 super(firmware_Cr50BID, self).initialize(host, cmdline_args, full_args, 122 restore_cr50_state=True, 123 cr50_dev_path=dev_path) 124 if self.cr50.using_ccd(): 125 raise error.TestNAError('Use a flex cable instead of CCD cable.') 126 127 if not self.cr50.has_command('bid'): 128 raise error.TestNAError('Cr50 image does not support board id') 129 130 # Save the necessary images. 131 self.dev_path = self.get_saved_cr50_dev_path() 132 133 self.image_versions = {} 134 135 original_version = self.get_saved_cr50_original_version() 136 self.save_universal_image(original_version) 137 self.save_board_id_locked_image(original_version, bid_path, release_ver) 138 139 # Clear the RLZ so ChromeOS doesn't set the board id during the updates. 140 cr50_utils.SetRLZ(self.host, '') 141 142 # Add tests to the test list based on the running board id infomation 143 self.build_tests() 144 145 # TODO(mruthven): remove once the test becomes more reliable. 146 # 147 # While tests randomly fail, keep this in so we can rerun individual 148 # tests. 149 self.test_subset = None 150 if test_subset: 151 self.test_subset = [int(case) for case in test_subset.split(',')] 152 153 154 def add_test(self, board_id, flags, expected_result): 155 """Add a test case to the list of tests 156 157 The test will see if the board id locked image behaves as expected with 158 the given board_id and flags. 159 160 Args: 161 board_id: A symbolic string or hex str representing the board id. 162 flags: a int value for the flags 163 expected_result: SUCCESS if the board id and flags should be 164 accepted by the board id locked image. BID_ERROR if it should be 165 rejected. 166 """ 167 logging.info('Test Case: image board id %s with chip board id %s:%x ' 168 'should %s', self.test_bid_str, board_id, flags, 169 'fail' if expected_result else 'succeed') 170 self.tests.append([board_id, flags, expected_result]) 171 172 173 def add_board_id_tests(self): 174 """Create a list of tests based on the board id and mask. 175 176 For each bit set to 1 in the board id image mask, Cr50 checks that the 177 bit in the board id infomask matches the image board id. Create a 178 couple of test cases based on the test mask and board id to verify this 179 behavior. 180 """ 181 mask_str = bin(self.test_mask).split('b')[1] 182 mask_str = '0' + mask_str if len(mask_str) < 32 else mask_str 183 mask_str = mask_str[::-1] 184 zero_index = mask_str.find('0') 185 one_index = mask_str.find('1') 186 187 # The hex version of the board id should be accepted. 188 self.add_test(hex(self.test_bid_int), self.test_flags, self.SUCCESS) 189 190 # Flip a bit we don't care about to make sure it is accepted 191 if zero_index != -1: 192 test_bid = self.test_bid_int ^ (1 << zero_index) 193 self.add_test(hex(test_bid), self.test_flags, self.SUCCESS) 194 195 196 if one_index != -1: 197 # Flip a bit we care about to make sure it is rejected 198 test_bid = self.test_bid_int ^ (1 << one_index) 199 self.add_test(hex(test_bid), self.test_flags, self.BID_ERROR) 200 else: 201 # If there is not a 1 in the board id mask, then we don't care about 202 # the board id at all. Flip all the bits and make sure setting the 203 # board id still succeeds. 204 test_bid = self.test_bid_int ^ self.MAX_BID 205 self.add_test(hex(test_bid), self.test_flags, self.SUCCESS) 206 207 208 def add_flag_tests(self): 209 """Create a list of tests based on the test flags. 210 211 When comparing the flag field, cr50 makes sure all 1s set in the image 212 flags are also set as 1 in the infomask. Create a couple of test cases 213 to verify cr50 responds appropriately to different flags. 214 """ 215 flag_str = bin(self.test_flags).split('b')[1] 216 flag_str_pad = '0' + flag_str if len(flag_str) < 32 else flag_str 217 flag_str_pad_rev = flag_str_pad[::-1] 218 zero_index = flag_str_pad_rev.find('0') 219 one_index = flag_str_pad_rev.find('1') 220 221 # If we care about any flag bits, setting the flags to 0 should cause 222 # a rejection 223 if self.test_flags: 224 self.add_test(self.test_bid_sym, 0, self.BID_ERROR) 225 226 # Flip a 0 to 1 to make sure it is accepted. 227 if zero_index != -1: 228 test_flags = self.test_flags | (1 << zero_index) 229 self.add_test(self.test_bid_sym, test_flags, self.SUCCESS) 230 231 # Flip a 1 to 0 to make sure it is rejected. 232 if one_index != -1: 233 test_flags = self.test_flags ^ (1 << one_index) 234 self.add_test(self.test_bid_sym, test_flags, self.BID_ERROR) 235 236 237 def build_tests(self): 238 """Add more test cases based on the image board id, flags, and mask""" 239 self.tests = self.BID_BASE_TESTS 240 self.add_flag_tests() 241 self.add_board_id_tests() 242 logging.info('Running tests %r', self.tests) 243 244 245 def save_universal_image(self, original_version, rw_ver=BID_SUPPORT): 246 """Get the non board id locked image 247 248 Save the universal image. Use the current cr50 image if it is not board 249 id locked. If the original image is board id locked, download a release 250 image from google storage. 251 252 Args: 253 original_version: The (ro ver, rw ver, and bid) of the running cr50 254 image. 255 rw_ver: The rw release version to use for the universal image. 256 """ 257 # If the original image is not board id locked, use it as universal 258 # image. If it is board id locked, use 0.3.4 as the universal image. 259 if not original_version[2]: 260 self.universal_path = self.get_saved_cr50_original_path() 261 universal_ver = original_version 262 else: 263 release_info = self.download_cr50_release_image(rw_ver) 264 self.universal_path, universal_ver = release_info 265 266 logging.info('Running test with universal image %s', universal_ver) 267 268 self.replace_image_if_newer(universal_ver[1], cr50_utils.CR50_PROD) 269 self.replace_image_if_newer(universal_ver[1], cr50_utils.CR50_PREPVT) 270 271 self.image_versions[self.UNIVERSAL] = universal_ver 272 273 274 def replace_image_if_newer(self, universal_rw_ver, path): 275 """Replace the image at path if it is newer than the universal image 276 277 Copy the universal image to path, if the universal image is older than 278 the image at path. 279 280 Args: 281 universal_rw_ver: The rw version string of the universal image 282 path: The path of the image that may need to be replaced. 283 """ 284 if self.host.path_exists(path): 285 dut_ver = cr50_utils.GetBinVersion(self.host, path)[1] 286 # If the universal version is lower than the DUT image, install the 287 # universal image. It has the lowest version of any image in the 288 # test, so cr50-update won't try to update cr50 at any point during 289 # the test. 290 install_image = (cr50_utils.GetNewestVersion(dut_ver, 291 universal_rw_ver) == dut_ver) 292 else: 293 # If the DUT doesn't have a file at path, install the image. 294 install_image = True 295 296 if install_image: 297 # Disable rootfs verification so we can copy the image to the DUT 298 self.rootfs_verification_disable() 299 # Copy the universal image onto the DUT. 300 dest, ver = cr50_utils.InstallImage(self.host, self.universal_path, 301 path) 302 logging.info('Copied %s to %s', ver, dest) 303 304 305 def save_board_id_locked_image(self, original_version, bid_path, 306 release_ver): 307 """Get the board id locked image 308 309 Save the board id locked image. Try to use the local path or test args 310 to find the release board id locked image. If those aren't valid, 311 fallback to using the running cr50 board id locked image or a debug 312 image with the TEST board id. 313 314 Args: 315 original_version: The (ro ver, rw ver, and bid) of the running cr50 316 image. 317 bid_path: the path to the board id locked image 318 release_ver: If given it will be used to download the release image 319 with the given rw version and board id 320 """ 321 if os.path.isfile(bid_path): 322 # If the bid_path exists, use that. 323 self.board_id_locked_path = bid_path 324 # Install the image on the device to get the image version 325 dest = os.path.join('/tmp', os.path.basename(bid_path)) 326 ver = cr50_utils.InstallImage(self.host, bid_path, dest)[1] 327 elif release_ver: 328 # Only use the release image if the release image is board id 329 # locked. 330 if '/' not in release_ver: 331 raise error.TestNAError('Release image is not board id locked.') 332 333 # split the release version into the rw string and board id string 334 release_rw, release_bid = release_ver.split('/', 1) 335 # Download a release image with the rw_version and board id 336 logging.info('Using %s %s release image for test', release_rw, 337 release_bid) 338 self.board_id_locked_path, ver = self.download_cr50_release_image( 339 release_rw, release_bid) 340 elif original_version[2]: 341 # If no valid board id args are given and the running image is 342 # board id locked, use it to run the test. 343 self.board_id_locked_path = self.get_saved_cr50_original_path() 344 ver = original_version 345 else: 346 devid = self.servo.get('cr50_devid') 347 self.board_id_locked_path, ver = self.download_cr50_debug_image( 348 devid, self.TEST_IMAGE_BID_INFO) 349 logging.info('Using %s DBG image for test', ver) 350 351 image_bid_info = cr50_utils.GetBoardIdInfoTuple(ver[2]) 352 if not image_bid_info: 353 raise error.TestError('Need board id locked image to run test') 354 # Save the image board id info 355 self.test_bid_int, self.test_mask, self.test_flags = image_bid_info 356 self.test_bid_sym = cr50_utils.GetSymbolicBoardId(self.test_bid_int) 357 self.test_bid_str = cr50_utils.GetBoardIdInfoString(ver[2]) 358 logging.info('Running test with bid locked image %s', ver) 359 self.image_versions[self.BID_LOCKED] = ver 360 361 362 def is_running_version(self, rw_ver, bid_str): 363 """Returns True if the running image has the same rw ver and bid 364 365 Args: 366 rw_ver: rw version string 367 bid_str: A symbolic or non-smybolic board id 368 369 Returns: 370 True if cr50 is running an image with the given rw version and 371 board id. 372 """ 373 running_rw = self.cr50.get_version() 374 running_bid = self.cr50.get_active_board_id_str() 375 # Convert the image board id to a non symbolic board id 376 bid_str = cr50_utils.GetBoardIdInfoString(bid_str, symbolic=False) 377 return running_rw == rw_ver and bid_str == running_bid 378 379 380 def reset_state(self, image_type): 381 """Update to the image and erase the board id. 382 383 We can't erase the board id unless we are running a debug image. Update 384 to the debug image so we can erase the board id and then rollback to the 385 right image. 386 387 Args: 388 image_type: the name of the image we want to be running at the end 389 of reset_state: 'universal' or 'board_id_locked'. This 390 image name needs to correspond with some test attribute 391 ${image_type}_path 392 393 Raises: 394 TestFail if the board id was not erased 395 """ 396 _, rw_ver, bid = self.image_versions[image_type] 397 chip_bid = cr50_utils.GetChipBoardId(self.host) 398 if self.is_running_version(rw_ver, bid) and (chip_bid == 399 cr50_utils.ERASED_CHIP_BID): 400 logging.info('Skipping reset. Already running %s image with erased ' 401 'chip board id', image_type) 402 return 403 logging.info('Updating to %s image and erasing chip bid', image_type) 404 405 self.cr50_update(self.dev_path) 406 407 # Rolling back will take care of erasing the board id 408 self.cr50_update(getattr(self, image_type + '_path'), rollback=True) 409 410 # Verify the board id was erased 411 if cr50_utils.GetChipBoardId(self.host) != cr50_utils.ERASED_CHIP_BID: 412 raise error.TestFail('Could not erase bid') 413 414 415 def updater_set_bid(self, bid, flags, exit_code): 416 """Set the flags using usb_updater and verify the result 417 418 Args: 419 board_id: board id string 420 flags: An int with the flag value 421 exit_code: the expected error code. 0 if it should succeed 422 423 Raises: 424 TestFail if usb_updater had an unexpected exit status or setting the 425 board id failed 426 """ 427 428 original_bid, _, original_flags = cr50_utils.GetChipBoardId(self.host) 429 430 if exit_code: 431 exit_code = 'Error %d while setting board id' % exit_code 432 433 try: 434 cr50_utils.SetChipBoardId(self.host, bid, flags) 435 result = self.SUCCESS 436 except error.AutoservRunError, e: 437 result = e.result_obj.stderr.strip() 438 439 if result != exit_code: 440 raise error.TestFail("Unexpected result setting %s:%x expected " 441 "'%s' got '%s'" % 442 (bid, flags, exit_code, result)) 443 444 # Verify cr50 is still running with the same board id and flags 445 if exit_code: 446 cr50_utils.CheckChipBoardId(self.host, original_bid, original_flags) 447 448 449 def run_bid_test(self, image_name, bid, flags, bid_error): 450 """Set the bid and flags. Verify a board id locked image response 451 452 Update to the right image type and try to set the board id. Only the 453 board id locked image should reject the given board id and flags. 454 455 If we are setting the board id on a non-board id locked image, try to 456 update to the board id locked image afterwards to verify that cr50 does 457 or doesn't rollback. If there is a bid error, cr50 should fail to update 458 to the board id locked image. 459 460 461 Args: 462 image_name: The image name 'universal', 'dev', or 'board_id_locked' 463 bid: A string representing the board id. Either the hex or symbolic 464 value 465 flags: A int value for the flags to set 466 bid_error: The expected usb_update error code. 0 for success 5 for 467 failure 468 """ 469 is_bid_locked_image = image_name == self.BID_LOCKED 470 471 # If the image is not board id locked, it should accept any board id and 472 # flags 473 exit_code = bid_error if is_bid_locked_image else self.SUCCESS 474 475 response = 'error %d' % exit_code if exit_code else 'success' 476 logging.info('EXPECT %s setting bid to %s:%x with %s image', 477 response, bid, flags, image_name) 478 479 # Erase the chip board id and update to the correct image 480 self.reset_state(image_name) 481 482 # Try to set the board id and flags 483 self.updater_set_bid(bid, flags, exit_code) 484 485 # If it failed before, it should fail with the same error. If we already 486 # set the board id, it should fail because the board id is already set. 487 self.updater_set_bid(bid, flags, exit_code if exit_code else 7) 488 489 # After setting the board id with a non boardid locked image, try to 490 # update to the board id locked image. Verify that cr50 does/doesn't run 491 # it. If there is a mismatch, the update should fail and Cr50 should 492 # rollback to the universal image. 493 if not is_bid_locked_image: 494 self.cr50_update(self.board_id_locked_path, 495 expect_rollback=(not not bid_error)) 496 497 498 def run_once(self): 499 """Verify the Cr50 BID response of each test bid.""" 500 errors = [] 501 for test_type, image_name in self.BID_TEST_TYPE: 502 logging.info('VERIFY: BID %s', test_type) 503 for i, args in enumerate(self.tests): 504 bid, flags, bid_error = args 505 # Replace place holder values with the test values 506 bid = bid if bid != None else self.test_bid_sym 507 flags = flags if flags != None else self.test_flags 508 message = '%s %d %s:%x %s' % (test_type, i, bid, flags, 509 bid_error) 510 511 if self.test_subset and i not in self.test_subset: 512 logging.info('Skipped %s', message) 513 continue 514 515 # Run the test with the given bid, flags, and result 516 try: 517 self.run_bid_test(image_name, bid, flags, bid_error) 518 logging.info('Verified %s', message) 519 except (error.TestFail, error.TestError) as e: 520 logging.info('FAILED %s with "%s"', message, e) 521 errors.append('%s with "%s"' % (message, e)) 522 if len(errors): 523 raise error.TestFail('failed tests: %s', errors) 524