1# Copyright 2018 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 os
6import re
7import logging
8
9from autotest_lib.server.cros.faft.fingerprint_test import FingerprintTest
10from autotest_lib.client.common_lib import error
11
12
13class firmware_Fingerprint(FingerprintTest):
14    """
15    Common class for running fingerprint firmware tests. Initializes the
16    firmware to a known state and then runs the test executable with
17    specified arguments on the DUT.
18    """
19    version = 1
20
21    def run_once(self, test_exe, test_exe_args=None,
22                 use_dev_signed_fw=False,
23                 enable_hardware_write_protect=True,
24                 enable_software_write_protect=True,
25                 force_firmware_flashing=False,
26                 init_entropy=True):
27        """Run the test."""
28        test_dir = os.path.join(self.bindir, 'tests/')
29        logging.info('test_dir: %s', test_dir)
30
31        # Initialize DUT state and set up tmp working directory on device.
32        self.setup_test(
33            test_dir, use_dev_signed_fw, enable_hardware_write_protect,
34            enable_software_write_protect, force_firmware_flashing,
35            init_entropy)
36
37        self._test_exe = test_exe
38
39        # Convert the arguments (test image names) to the actual filenames of
40        # the test images.
41        image_args = []
42        if test_exe_args:
43            for arg in test_exe_args:
44                image_args.append(getattr(self, arg))
45        self._test_exe_args = image_args
46
47        if self.get_host_board() == 'zork':
48            # TODO(b/170770251): Move the rdp1 and rdp0 tests to separate files
49            #
50            # Zork's RDP1 and RDP0 tests requires an AP reboot, so do it in
51            # this class
52            if self._test_exe == 'rdp1.sh':
53                self.test_rdp1()
54            elif self._test_exe == 'rdp0.sh':
55                self.test_rdp0()
56        else:
57            logging.info('Running test: %s', self._test_exe)
58            self.run_test(self._test_exe, *self._test_exe_args)
59
60    def test_rdp1(self):
61        """
62        Validate initial state for the RDP1 test. The test tries to read from
63        flash while maintaining RDP level 1. Then it tries to read from flash
64        while changing RDP level to 0.
65        """
66        if self.get_fp_board() == 'bloonchipper':
67            _HW_WP_OFF_AND_SW_WP_ON = (
68                    'Flash protect flags: 0x00000407 ro_at_boot ro_now rollback_now all_now\n'
69                    'Valid flags:         0x0000003f wp_gpio_asserted ro_at_boot ro_now all_now STUCK INCONSISTENT\n'
70                    'Writable flags:      0x00000000\n')
71        else:
72            _HW_WP_OFF_AND_SW_WP_ON = (
73                    'Flash protect flags: 0x00000003 ro_at_boot ro_now\n'
74                    'Valid flags:         0x0000003f wp_gpio_asserted ro_at_boot ro_now all_now STUCK INCONSISTENT\n'
75                    'Writable flags:      0x00000000\n')
76
77        logging.info('Running test to validate RDP level 1')
78        original_fw_file = self._test_exe_args[0]
79        self.check_file_exists(original_fw_file)
80
81        logging.info('Making sure hardware write protect is DISABLED and '
82                     'software write protect is ENABLED')
83        flashprotect_result = self._run_ectool_cmd('flashprotect')
84        if flashprotect_result.stdout != _HW_WP_OFF_AND_SW_WP_ON:
85            raise error.TestFail('Incorrect flashprotect state')
86
87        logging.info('Validating initial state')
88        # TODO(yichengli): Check that we are running MP-signed RO and RW by
89        # checking the key id.
90        if self.get_running_firmware_type() != self._FIRMWARE_TYPE_RW:
91            raise error.TestFail('Not running RW copy of firmware')
92        if not self.is_rollback_set_to_initial_val():
93            raise error.TestFail('Rollback is not set to initial value')
94
95        self.test_rdp1_without_modifying_rdp_level()
96        self.test_rdp1_while_setting_rdp_level_0()
97
98    def test_rdp0(self):
99        """
100        Validate initial state for the RDP0 test. The test tries to read from
101        flash while maintaining RDP level 0. Then it tries to read from flash
102        while setting RDP level to 0.
103        """
104        _HW_AND_SW_WP_OFF = (
105                'Flash protect flags: 0x00000000\n'
106                'Valid flags:         0x0000003f wp_gpio_asserted ro_at_boot ro_now all_now STUCK INCONSISTENT\n'
107                'Writable flags:      0x00000001 ro_at_boot\n')
108
109        logging.info('Running test to validate RDP level 0')
110        original_fw_file = self._test_exe_args[0]
111        self.check_file_exists(original_fw_file)
112
113        logging.info('Making sure all write protect is disabled')
114        flashprotect_result = self._run_ectool_cmd('flashprotect')
115        if flashprotect_result.stdout != _HW_AND_SW_WP_OFF:
116            raise error.TestFail('Incorrect flashprotect state')
117
118        logging.info('Validating initial state')
119        # TODO(yichengli): Check that we are running MP-signed RO and RW by
120        # checking the key id.
121        if self.get_running_firmware_type() != self._FIRMWARE_TYPE_RW:
122            raise error.TestFail('Not running RW copy of firmware')
123        if not self.is_rollback_unset():
124            raise error.TestFail('Rollback should be unset.')
125
126        self.check_firmware_is_functional()
127
128        self.test_rdp0_without_modifying_rdp_level()
129        self.test_rdp0_while_setting_rdp_level_0()
130
131    def test_rdp1_without_modifying_rdp_level(self):
132        """
133        Given:
134           * Hardware write protect is disabled
135               (so we can use bootloader to read and change RDP level)
136           * Software write protect is enabled
137           * RDP is at level 1
138
139        Then:
140           * Reading from flash without changing the RDP level should fail
141             (and we should not have read any bytes from flash).
142           * The firmware should still be functional because mass erase is NOT
143             triggered since we are NOT changing the RDP level.
144        """
145        logging.info('Reading firmware without modifying RDP level')
146
147        # This should fail and the file should be empty
148        file_read_from_flash = os.path.join(self._dut_working_dir,
149                                            'test_keep_rdp.bin')
150        cmd = 'flash_fp_mcu --read --noremove_flash_read_protect %s' % file_read_from_flash
151        result = self.run_cmd(cmd)
152        if result.exit_status == 0:
153            raise error.TestFail('Should not be able to read from flash')
154
155        logging.info('Checking file_read_from_flash is empty')
156        if self.get_file_size(file_read_from_flash) != 0:
157            raise error.TestFail('File read from flash is not empty')
158
159        # On zork, an AP reboot is needed after using flash_fp_mcu.
160        if self.get_host_board() == 'zork':
161            self.host.reboot()
162
163        self.check_firmware_is_functional()
164
165    def test_rdp1_while_setting_rdp_level_0(self):
166        """
167        Given:
168           * Hardware write protect is disabled
169               (so we can use bootloader to read and change RDP level)
170           * Software write protect is enabled
171           * RDP is at level 1
172
173        Then:
174           * Setting the RDP level to 0 (after being at level 1) should trigger
175             a mass erase.
176           * A mass erase sets all flash bytes to 0xFF, so all bytes read from flash
177             should have that value.
178           * Since the flash was mass erased, the firmware should no longer function.
179        """
180        logging.info('Reading firmware after setting RDP to level 0')
181
182        # This command partially fails (and returns an error) because it causes the
183        # flash to be mass erased, but we should still have a file with the contents
184        # that we can compare against.
185
186        file_read_from_flash = os.path.join(self._dut_working_dir,
187                                            'test_change_rdp.bin')
188        cmd = 'flash_fp_mcu --read %s' % file_read_from_flash
189        self.run_cmd(cmd)
190
191        logging.info(
192                'Checking that value read is made up entirely of OxFF bytes')
193        original_fw_file = self._test_exe_args[0]
194        if self.get_file_size(original_fw_file) != self.get_file_size(
195                file_read_from_flash):
196            raise error.TestFail(
197                    'Flash read output size doesn\'t match original fw size')
198        self.check_file_contains_all_0xFF_bytes(file_read_from_flash)
199
200        # On zork, an AP reboot is needed after using flash_fp_mcu.
201        if self.get_host_board() == 'zork':
202            self.host.reboot()
203
204        logging.info('Checking that firmware is non-functional')
205        result = self._run_ectool_cmd('version')
206        if result.exit_status == 0:
207            raise error.TestFail(
208                    'Firmware should not be responding to commands')
209
210    def test_rdp0_without_modifying_rdp_level(self):
211        """
212        Given:
213           * Hardware write protect is disabled
214           * Software write protect is disabled
215           * RDP is at level 0
216
217        Then:
218           * Reading from flash without changing the RDP level should succeed
219             (we're already at level 0). Thus we should be able to read the
220             entire firmware out of flash and it should exactly match the
221             firmware that we flashed for testing.
222        """
223        logging.info('Reading firmware without modifying RDP level')
224
225        file_read_from_flash = os.path.join(self._dut_working_dir,
226                                            'test_keep_rdp.bin')
227        cmd = 'flash_fp_mcu --read --noremove_flash_read_protect %s' % file_read_from_flash
228        result = self.run_cmd(cmd)
229        if result.exit_status != 0:
230            raise error.TestFail('Failed to read from flash')
231
232        logging.info('Checking that value read matches the flashed version')
233        original_fw_file = self._test_exe_args[0]
234        if not self.files_match(file_read_from_flash, original_fw_file):
235            raise error.TestFail(
236                    'File read from flash does not match original fw file')
237
238        # On zork, an AP reboot is needed after using flash_fp_mcu.
239        if self.get_host_board() == 'zork':
240            self.host.reboot()
241
242        self.check_firmware_is_functional()
243
244    def test_rdp0_while_setting_rdp_level_0(self):
245        """
246        Given:
247           * Hardware write protect is disabled
248           * Software write protect is disabled
249           * RDP is at level 0
250
251        Then:
252           * Changing the RDP level to 0 should have no effect
253             (we're already at level 0). Thus we should be able to read the
254             entire firmware out of flash and it should exactly match the
255             firmware that we flashed for testing.
256        """
257        logging.info('Reading firmware while setting RDP to level 0')
258
259        file_read_from_flash = os.path.join(self._dut_working_dir,
260                                            'test_change_rdp.bin')
261        cmd = 'flash_fp_mcu --read %s' % file_read_from_flash
262        result = self.run_cmd(cmd)
263        if result.exit_status != 0:
264            raise error.TestFail('Failed to read from flash')
265
266        logging.info('Checking that value read matches the flashed version')
267        original_fw_file = self._test_exe_args[0]
268        if not self.files_match(file_read_from_flash, original_fw_file):
269            raise error.TestFail(
270                    'File read from flash does not match original fw file')
271
272        # On zork, an AP reboot is needed after using flash_fp_mcu.
273        if self.get_host_board() == 'zork':
274            self.host.reboot()
275
276        self.check_firmware_is_functional()
277
278    def check_file_exists(self, filename):
279        """Checks that |filename| exists on DUT. Fails the test otherwise."""
280        if not self.host.is_file_exists(filename):
281            raise error.TestFail('Cannot find file: %s' % filename)
282
283    def get_file_size(self, filename):
284        """Returns the size of |filename| on DUT. Fails the test on error."""
285        cmd = 'stat --printf %%s %s' % filename
286        result = self.run_cmd(cmd)
287        if result.exit_status != 0 or not result.stdout.isdigit():
288            raise error.TestFail('Cannot get the size of file: %s' % filename)
289        return int(result.stdout)
290
291    def files_match(self, filename1, filename2):
292        """Returns True if two files are identical, False otherwise."""
293        cmd = 'cmp %s %s' % (filename1, filename2)
294        return self.run_cmd(cmd).exit_status == 0
295
296    def check_file_contains_all_0xFF_bytes(self, file_to_check):
297        """
298        Checks that |file_to_check| is made of only 0xFF bytes.
299        Fails the test otherwise.
300        """
301        regex = '0000000 ffff ffff ffff ffff ffff ffff ffff ffff\n\*\n[0-9]+\n$'
302        cmd = 'hexdump %s' % file_to_check
303        result = self.run_cmd(cmd)
304        if not re.match(regex, result.stdout):
305            raise error.TestFail('%s does not contain all 0xFF bytes' %
306                                 file_to_check)
307
308    def check_firmware_is_functional(self):
309        """
310        Returns true if AP can talk to FPMCU firmware. Fails the test otherwise
311        """
312        logging.info('Checking that firmware is functional')
313        # Catch exception to show better error message.
314        try:
315            self.get_running_firmware_type()
316        except error.TestFail:
317            raise error.TestFail('Firmware is not functional')
318