Home | History | Annotate | Download | only in firmware_Cr50BID
      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 
      5 import logging
      6 import os
      7 
      8 from autotest_lib.client.common_lib import error
      9 from autotest_lib.client.common_lib.cros import cr50_utils, tpm_utils
     10 from autotest_lib.server.cros.faft.cr50_test import Cr50Test
     11 
     12 
     13 class 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. Use this as the universal image if the
     56     # image running on the device is board id locked.
     57     BID_SUPPORT = '0.0.21'
     58 
     59     # Board id locked debug files will use the board id, mask, and flags in the
     60     # gs filename
     61     TEST_BOARD_ID = 'TEST'
     62     TEST_MASK = 0xffff
     63     TEST_FLAGS = 0xff00
     64     TEST_IMAGE_BID_INFO = [TEST_BOARD_ID, TEST_MASK, TEST_FLAGS]
     65     BID_MISMATCH = ['Board ID mismatched, but can not reboot.']
     66     BID_ERROR = 5
     67     SUCCESS = 0
     68 
     69     # BID_BASE_TESTS is a list with the the board id and flags to test for each
     70     # run. Each item in the list is a list of [board_id, flags, exit status].
     71     # exit_status should be BID_ERROR if the board id and flags should not be
     72     # compatible with the board id locked image.
     73     #
     74     # A image without board id will be able to run on a device with all of the
     75     # board id and flag combinations.
     76     #
     77     # When using a non-symbolic board id, make sure the length of the string is
     78     # greater than 4. If the string length is less than 4, usb_updater will
     79     # treat it as a symbolic string
     80     # ex: bid of 0 needs to be given as '0x0000'. If it were given as '0', the
     81     # board id value would be interpreted as ord('0')
     82     #
     83     # These base tests are be true no matter the board id, mask, or flags. If a
     84     # value is None, then it will be replaced with the test board id or flags
     85     # while running the test.
     86     BID_BASE_TESTS = [
     87         [None, None, SUCCESS],
     88 
     89         # All 1s in the board id flags should be acceptable no matter the
     90         # actual image flags
     91         [None, MAX_BID, SUCCESS],
     92     ]
     93 
     94     # Settings to test all of the cr50 BID responses. The dictionary conatins
     95     # the name of the BID verification as the key and a list as a value.
     96     #
     97     # The value of the list is the image to start running the test with then
     98     # the method to update to the board id locked image as the value.
     99     #
    100     # If the start image is 'board_id_locked', we won't try to update to the
    101     # board id locked image.
    102     BID_TEST_TYPE = [
    103         # Verify that the board id locked image rejects invalid board ids
    104         ['get/set', BID_LOCKED],
    105 
    106         # Verify the cr50 response when doing a normal update to a board id
    107         # locked image. If there is a board id mismatch, cr50 should rollback
    108         # to the image that was already running.
    109         ['rollback', UNIVERSAL],
    110 
    111         # TODO (mruthven): add support for verifying recovery
    112         # Certain devices are not able to successfully jump to the recovery
    113         # image when the TPM is locked down. We need to find a way to verify the
    114         # DUT is in recovery without being able to ssh into the DUT.
    115     ]
    116 
    117     def initialize(self, host, cmdline_args, dev_path='', bid_path='',
    118                    release_ver=None, test_subset=None):
    119         # Restore the original image, rlz code, and board id during cleanup.
    120         super(firmware_Cr50BID, self).initialize(host, cmdline_args,
    121                                                  restore_cr50_state=True,
    122                                                  cr50_dev_path=dev_path)
    123         if self.cr50.using_ccd():
    124             raise error.TestNAError('Use a flex cable instead of CCD cable.')
    125 
    126         if not self.cr50.has_command('bid'):
    127             raise error.TestNAError('Cr50 image does not support board id')
    128 
    129         # Save the necessary images.
    130         self.dev_path = self.get_saved_cr50_dev_path()
    131 
    132         self.image_versions = {}
    133 
    134         original_version = self.get_saved_cr50_original_version()
    135         self.save_universal_image(original_version)
    136         self.save_board_id_locked_image(original_version, bid_path, release_ver)
    137 
    138         # Clear the RLZ so ChromeOS doesn't set the board id during the updates.
    139         cr50_utils.SetRLZ(self.host, '')
    140 
    141         # Add tests to the test list based on the running board id infomation
    142         self.build_tests()
    143 
    144         # TODO(mruthven): remove once the test becomes more reliable.
    145         #
    146         # While tests randomly fail, keep this in so we can rerun individual
    147         # tests.
    148         self.test_subset = None
    149         if test_subset:
    150             self.test_subset = [int(case) for case in test_subset.split(',')]
    151 
    152 
    153     def add_test(self, board_id, flags, expected_result):
    154         """Add a test case to the list of tests
    155 
    156         The test will see if the board id locked image behaves as expected with
    157         the given board_id and flags.
    158 
    159         Args:
    160             board_id: A symbolic string or hex str representing the board id.
    161             flags: a int value for the flags
    162             expected_result: SUCCESS if the board id and flags should be
    163                 accepted by the board id locked image. BID_ERROR if it should be
    164                 rejected.
    165         """
    166         logging.info('Test Case: image board id %s with chip board id %s:%x '
    167                      'should %s', self.test_bid_str, board_id, flags,
    168                      'fail' if expected_result else 'succeed')
    169         self.tests.append([board_id, flags, expected_result])
    170 
    171 
    172     def add_board_id_tests(self):
    173         """Create a list of tests based on the board id and mask.
    174 
    175         For each bit set to 1 in the board id image mask, Cr50 checks that the
    176         bit in the board id infomask matches the image board id. Create a
    177         couple of test cases based on the test mask and board id to verify this
    178         behavior.
    179         """
    180         bid_int = cr50_utils.ConvertSymbolicBoardId(self.test_bid)
    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 symbolic string should be accepted.
    188         self.add_test(hex(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 = 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 = 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 = 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, 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, 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, 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.0.21 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, self.test_mask, self.test_flags = image_bid_info
    356         self.test_bid_str = cr50_utils.GetBoardIdInfoString(ver[2])
    357         logging.info('Running test with bid locked image %s', ver)
    358         self.image_versions[self.BID_LOCKED] = ver
    359 
    360 
    361     def cleanup(self):
    362         """Clear the TPM Owner"""
    363         super(firmware_Cr50BID, self).cleanup()
    364         tpm_utils.ClearTPMOwnerRequest(self.host)
    365 
    366 
    367     def is_running_version(self, rw_ver, bid_str):
    368         """Returns True if the running image has the same rw ver and bid
    369 
    370         Args:
    371             rw_ver: rw version string
    372             bid_str: A symbolic or non-smybolic board id
    373 
    374         Returns:
    375             True if cr50 is running an image with the given rw version and
    376             board id.
    377         """
    378         running_rw = self.cr50.get_version()
    379         running_bid = self.cr50.get_active_board_id_str()
    380         # Convert the image board id to a non symbolic board id
    381         bid_str = cr50_utils.GetBoardIdInfoString(bid_str, symbolic=False)
    382         return running_rw == rw_ver and bid_str == running_bid
    383 
    384 
    385     def reset_state(self, image_type):
    386         """Update to the image and erase the board id.
    387 
    388         We can't erase the board id unless we are running a debug image. Update
    389         to the debug image so we can erase the board id and then rollback to the
    390         right image.
    391 
    392         Args:
    393             image_type: the name of the image we want to be running at the end
    394                         of reset_state: 'universal' or 'board_id_locked'. This
    395                         image name needs to correspond with some test attribute
    396                         ${image_type}_path
    397 
    398         Raises:
    399             TestFail if the board id was not erased
    400         """
    401         _, rw_ver, bid = self.image_versions[image_type]
    402         chip_bid = cr50_utils.GetChipBoardId(self.host)
    403         if self.is_running_version(rw_ver, bid) and (chip_bid ==
    404             cr50_utils.ERASED_CHIP_BID):
    405             logging.info('Skipping reset. Already running %s image with erased '
    406                 'chip board id', image_type)
    407             return
    408         logging.info('Updating to %s image and erasing chip bid', image_type)
    409 
    410         self.cr50_update(self.dev_path)
    411 
    412         # Rolling back will take care of erasing the board id
    413         self.cr50_update(getattr(self, image_type + '_path'), rollback=True)
    414 
    415         # Verify the board id was erased
    416         if cr50_utils.GetChipBoardId(self.host) != cr50_utils.ERASED_CHIP_BID:
    417             raise error.TestFail('Could not erase bid')
    418 
    419 
    420     def updater_set_bid(self, bid, flags, exit_code):
    421         """Set the flags using usb_updater and verify the result
    422 
    423         Args:
    424             board_id: board id string
    425             flags: An int with the flag value
    426             exit_code: the expected error code. 0 if it should succeed
    427 
    428         Raises:
    429             TestFail if usb_updater had an unexpected exit status or setting the
    430             board id failed
    431         """
    432 
    433         original_bid, _, original_flags = cr50_utils.GetChipBoardId(self.host)
    434 
    435         if exit_code:
    436             exit_code = 'Error %d while setting board id' % exit_code
    437 
    438         try:
    439             cr50_utils.SetChipBoardId(self.host, bid, flags)
    440             result = self.SUCCESS
    441         except error.AutoservRunError, e:
    442             result = e.result_obj.stderr.strip()
    443 
    444         if result != exit_code:
    445             raise error.TestFail("Unexpected result setting %s:%x expected "
    446                                  "'%s' got '%s'" %
    447                                  (bid, flags, exit_code, result))
    448 
    449         # Verify cr50 is still running with the same board id and flags
    450         if exit_code:
    451             cr50_utils.CheckChipBoardId(self.host, original_bid, original_flags)
    452 
    453 
    454     def run_bid_test(self, image_name, bid, flags, bid_error):
    455         """Set the bid and flags. Verify a board id locked image response
    456 
    457         Update to the right image type and try to set the board id. Only the
    458         board id locked image should reject the given board id and flags.
    459 
    460         If we are setting the board id on a non-board id locked image, try to
    461         update to the board id locked image afterwards to verify that cr50 does
    462         or doesn't rollback. If there is a bid error, cr50 should fail to update
    463         to the board id locked image.
    464 
    465 
    466         Args:
    467             image_name: The image name 'universal', 'dev', or 'board_id_locked'
    468             bid: A string representing the board id. Either the hex or symbolic
    469                  value
    470             flags: A int value for the flags to set
    471             bid_error: The expected usb_update error code. 0 for success 5 for
    472                        failure
    473         """
    474         is_bid_locked_image = image_name == self.BID_LOCKED
    475 
    476         # If the image is not board id locked, it should accept any board id and
    477         # flags
    478         exit_code = bid_error if is_bid_locked_image else self.SUCCESS
    479 
    480         response = 'error %d' % exit_code if exit_code else 'success'
    481         logging.info('EXPECT %s setting bid to %s:%x with %s image',
    482                      response, bid, flags, image_name)
    483 
    484         # Erase the chip board id and update to the correct image
    485         self.reset_state(image_name)
    486 
    487         # Try to set the board id and flags
    488         self.updater_set_bid(bid, flags, exit_code)
    489 
    490         # If it failed before, it should fail with the same error. If we already
    491         # set the board id, it should fail because the board id is already set.
    492         self.updater_set_bid(bid, flags, exit_code if exit_code else 7)
    493 
    494         # After setting the board id with a non boardid locked image, try to
    495         # update to the board id locked image. Verify that cr50 does/doesn't run
    496         # it. If there is a mismatch, the update should fail and Cr50 should
    497         # rollback to the universal image.
    498         if not is_bid_locked_image:
    499             self.cr50_update(self.board_id_locked_path,
    500                              expect_rollback=(not not bid_error))
    501 
    502 
    503     def run_once(self):
    504         """Verify the Cr50 BID response of each test bid."""
    505         errors = []
    506         for test_type, image_name in self.BID_TEST_TYPE:
    507             logging.info('VERIFY: BID %s', test_type)
    508             for i, args in enumerate(self.tests):
    509                 bid, flags, bid_error = args
    510                 # Replace place holder values with the test values
    511                 bid = bid if bid != None else self.test_bid
    512                 flags = flags if flags != None else self.test_flags
    513                 message = '%s %d %s:%x %s' % (test_type, i, bid, flags,
    514                     bid_error)
    515 
    516                 if self.test_subset and i not in self.test_subset:
    517                     logging.info('Skipped %s', message)
    518                     continue
    519 
    520                 # Run the test with the given bid, flags, and result
    521                 try:
    522                     self.run_bid_test(image_name, bid, flags, bid_error)
    523                     logging.info('Verified %s', message)
    524                 except (error.TestFail, error.TestError) as e:
    525                     logging.info('FAILED %s with "%s"', message, e)
    526                     errors.append('%s with "%s"' % (message, e))
    527         if len(errors):
    528             raise error.TestFail('failed tests: %s', errors)
    529