• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, tpm_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. 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