xref: /aosp_15_r20/external/autotest/server/site_tests/firmware_Cr50RMAOpen/firmware_Cr50RMAOpen.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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 logging
6import re
7import time
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib.cros import cr50_utils
11from autotest_lib.server.cros.faft.cr50_test import Cr50Test
12
13
14class firmware_Cr50RMAOpen(Cr50Test):
15    """Verify Cr50 RMA behavoior
16
17    Verify a couple of things:
18        - basic open from AP and command line
19        - Rate limiting
20        - Authcodes can't be reused once another challenge is generated.
21        - if the image is prod signed with mp flags, it isn't using test keys
22
23    Generate the challenge and calculate the response using rma_reset -c. Verify
24    open works and enables all of the ccd features.
25
26    If the generated challenge has the wrong version, make sure the challenge
27    generated with the test key fails.
28    """
29    version = 1
30
31    # Tuple representing WP state when it is force disabled
32    WP_PERMANENTLY_DISABLED = (False, False, False, False)
33
34    # Various Error Messages from the command line and AP RMA failures
35    MISMATCH_CLI = 'Auth code does not match.'
36    MISMATCH_AP = 'rma unlock failed, code 6'
37    # Starting in 0.4.8 cr50 doesn't print "RMA Auth error 0x504". It doesn't
38    # print anything. Once prod and prepvt versions do this remove the error
39    # code from the test.
40    LIMIT_CLI = '(RMA Auth error 0x504|rma_auth\s+>)'
41    LIMIT_AP = 'error 4'
42    ERR_DISABLE_AP = 'error 7'
43    # GSCTool exit statuses
44    UPDATE_ERROR = 3
45    SUCCESS = 0
46    # Cr50 limits generating challenges to once every 10 seconds
47    CHALLENGE_INTERVAL = 10
48    SHORT_WAIT = 3
49    # Cr50 RMA commands can be sent from the AP or command line. They should
50    # behave the same and be interchangeable
51    CMD_INTERFACES = ['ap', 'cli']
52
53    def initialize(self, host, cmdline_args, full_args):
54        """Initialize the test"""
55        super(firmware_Cr50RMAOpen, self).initialize(host, cmdline_args,
56                full_args)
57        self.host = host
58
59        if not hasattr(self, 'cr50'):
60            raise error.TestNAError('Test can only be run on devices with '
61                                    'access to the Cr50 console')
62
63        if not self.cr50.has_command('rma_auth'):
64            raise error.TestNAError('Cannot test on Cr50 without RMA support')
65
66        if not self.cr50._servo.dts_mode_is_valid():
67            raise error.TestNAError('This messes with ccd settings. Use flex '
68                    'cable to run the test.')
69
70        if self.host.run('rma_reset -h', ignore_status=True).exit_status == 127:
71            raise error.TestNAError('Cannot test RMA open without rma_reset')
72
73        # Disable all capabilities at the start of the test. Go ahead and enable
74        # testlab mode if it isn't enabled.
75        self.fast_ccd_open(enable_testlab=True)
76        self.cr50.ccd_reset(servo_en=False)
77        self.cr50.set_ccd_level('lock')
78        # Make sure all capabilities are set to default.
79        try:
80            self.check_ccd_cap_settings(False)
81        except error.TestFail:
82            raise error.TestError('Could not disable rma mode')
83
84        self.is_prod_mp = self.get_prod_mp_status()
85
86
87    def get_prod_mp_status(self):
88        """Returns True if Cr50 is running a prod signed mp flagged image"""
89        # Determine if the running image is using premp flags
90        bid = self.cr50.get_active_board_id_str()
91        premp_flags = int(bid.split(':')[2], 16) & 0x10 if bid else False
92
93        # Check if the running image is signed with prod keys
94        prod_keys = self.cr50.using_prod_rw_keys()
95        logging.info('%s keys with %s flags', 'prod' if prod_keys else 'dev',
96                'premp' if premp_flags else 'mp')
97        return not premp_flags and prod_keys
98
99
100    def parse_challenge(self, challenge):
101        """Remove the whitespace from the challenge"""
102        return re.sub('\s', '', challenge.strip())
103
104
105    def generate_response(self, challenge):
106        """Generate the authcode from the challenge.
107
108        Args:
109            challenge: The Cr50 challenge string
110
111        Returns:
112            A tuple of the authcode and a bool True if the response should
113            work False if it shouldn't
114        """
115        stdout = self.host.run('rma_reset -c ' + challenge).stdout
116        logging.info(stdout)
117        # rma_reset generates authcodes with the test key. MP images should use
118        # prod keys. Make sure prod signed MP images aren't using the test key.
119        self.prod_rma_key = 'Unsupported' in stdout
120        if self.is_prod_mp and not self.prod_rma_key:
121            raise error.TestFail('MP image cannot use test key')
122        return re.search('Authcode: +(\S+)', stdout).group(1), self.prod_rma_key
123
124
125    def rma_cli(self, authcode='', disable=False, expected_exit_status=SUCCESS):
126        """Run RMA commands using the command line.
127
128        Args:
129            authcode: The authcode string
130            disable: True if RMA open should be disabled.
131            expected_exit_status: the expected exit status
132
133        Returns:
134            The entire stdout from the command or the RMA challenge
135        """
136        cmd = 'rma_auth ' + ('disable' if disable else authcode)
137        get_challenge = not (authcode or disable)
138        resp = 'rma_auth(.*generated challenge:)?(.*)>'
139        if expected_exit_status:
140            resp = self.LIMIT_CLI if get_challenge else self.MISMATCH_CLI
141
142        result = self.cr50.send_command_get_output(cmd, [resp])
143        logging.info(result)
144        return (self.parse_challenge(result[0][-1]) if get_challenge else
145                result[0])
146
147
148    def rma_ap(self, authcode='', disable=False, expected_exit_status=SUCCESS):
149        """Run RMA commands using vendor commands from the ap.
150
151        Args:
152            authcode: the authcode string.
153            disable: True if RMA open should be disabled.
154            expected_exit_status: the expected exit status
155
156        Returns:
157            The entire stdout from the command or the RMA challenge
158
159        Raises:
160            error.TestFail if there is an unexpected gsctool response
161        """
162        if disable:
163            cmd = '-a -F disable'
164        else:
165            cmd = '-a -r ' + authcode
166        get_challenge = not (authcode or disable)
167
168        expected_stderr = ''
169        if expected_exit_status:
170            if authcode:
171                expected_stderr = self.MISMATCH_AP
172            elif disable:
173                expected_stderr = self.ERR_DISABLE_AP
174            else:
175                expected_stderr = self.LIMIT_AP
176
177        result = cr50_utils.GSCTool(self.host, cmd.split(),
178                ignore_status=expected_stderr)
179        logging.info(result)
180        # Various connection issues result in warnings. If there is a real issue
181        # the expected_exit_status will raise it. Ignore any warning messages in
182        # stderr.
183        ignore_stderr = 'WARNING' in result.stderr and not expected_stderr
184        if not ignore_stderr and expected_stderr not in result.stderr.strip():
185            raise error.TestFail('Unexpected stderr: expected %s got %s' %
186                    (expected_stderr, result.stderr.strip()))
187        if result.exit_status != expected_exit_status:
188            raise error.TestFail('Unexpected exit_status: expected %s got %s' %
189                    (expected_exit_status, result.exit_status))
190        if get_challenge:
191            return self.parse_challenge(result.stdout.split('Challenge:')[-1])
192        return result.stdout
193
194
195    def fake_rma_open(self):
196        """Use individual commands to enter the same state as factory mode"""
197        self.cr50.send_command('ccd testlab open')
198        self.cr50.ccd_reset_factory()
199        self.cr50.send_command('wp disable atboot')
200
201
202    def check_ccd_cap_settings(self, rma_opened):
203        """Verify the ccd capability permissions match the RMA state
204
205        Args:
206            rma_opened: True if we expect Cr50 to be RMA opened
207
208        Raises:
209            TestFail if Cr50 is opened when it should be closed or it is closed
210            when it should be opened.
211        """
212        time.sleep(self.SHORT_WAIT)
213        caps = self.cr50.get_cap_dict()
214        in_factory_mode, reset = self.cr50.get_cap_overview(caps)
215
216        if rma_opened and not in_factory_mode:
217            raise error.TestFail('Not all capablities were set to Always')
218        if not rma_opened and not reset:
219            raise error.TestFail('Not all capablities were set to Default')
220
221
222    def rma_open(self, challenge_func, auth_func):
223        """Run the RMA open process
224
225        Run the RMA open process with the given functions. Use challenge func
226        to generate the challenge and auth func to verify the authcode. The
227        commands can be sent from the command line or ap. Both should be able
228        to be used as the challenge or auth function interchangeably.
229
230        Args:
231            challenge_func: The method used to generate the challenge
232            auth_func: The method used to verify the authcode
233        """
234        time.sleep(self.CHALLENGE_INTERVAL)
235
236        # Get the challenge
237        challenge = challenge_func()
238        logging.info(challenge)
239
240        # Try using the challenge. If the Cr50 KeyId is not supported, make sure
241        # RMA open fails.
242        authcode, unsupported_key = self.generate_response(challenge)
243        exit_status = self.UPDATE_ERROR if unsupported_key else self.SUCCESS
244
245        # Attempt RMA open with the given authcode
246        auth_func(authcode=authcode, expected_exit_status=exit_status)
247
248        # Make sure ccd is in the proper state. If the RMA key is prod, the test
249        # wont be able to generate the authcode and ccd should still be reset.
250        # It should not be in factory mode.
251        if unsupported_key:
252            self.confirm_ccd_is_reset()
253        else:
254            self.confirm_ccd_is_in_factory_mode()
255
256        self.host.reboot()
257
258        if not self.tpm_is_responsive():
259            raise error.TestFail('TPM was not reenabled after reboot')
260
261        # Run RMA disable to reset the capabilities.
262        self.rma_ap(disable=True, expected_exit_status=exit_status)
263
264        self.confirm_ccd_is_reset()
265
266
267    def confirm_ccd_is_in_factory_mode(self, check_tpm=True):
268        """Check wp and capabilities to confirm cr50 is in factory mode"""
269        # The open process takes some time to complete. Wait for it.
270        time.sleep(self.CHALLENGE_INTERVAL)
271
272        if check_tpm and self.tpm_is_responsive():
273            raise error.TestFail('TPM was not disabled after RMA open')
274
275        if self.cr50.get_wp_state() != self.WP_PERMANENTLY_DISABLED:
276            raise error.TestFail('HW WP was not disabled after RMA open')
277
278        # Make sure capabilities are all set to Always
279        self.check_ccd_cap_settings(True)
280
281
282    def confirm_ccd_is_reset(self):
283        """Check wp and capabilities to confirm ccd has been reset"""
284        # The open process takes some time to complete. Wait for it.
285        time.sleep(self.CHALLENGE_INTERVAL)
286
287        if not self.tpm_is_responsive():
288            raise error.TestFail('TPM is disabled')
289
290        # Confirm write protect has been reset to follow battery presence. The
291        # WP state may be enabled or disabled. The state just can't be forced.
292        if not self.cr50.wp_is_reset():
293            raise error.TestFail('Factory mode disable did not reset HW WP')
294
295        # Make sure capabilities have been reset
296        self.check_ccd_cap_settings(False)
297
298
299    def verify_basic_factory_disable(self):
300        """Verify RMA disable works.
301
302        The RMA open process may not be able to be automated, because it
303        requires phyiscal presence and access to the server. This uses console
304        commands to enter the same state as factory mode and then verifies
305        rma disable resets all of that.
306        """
307        self.fake_rma_open()
308
309        # There's no way to disable the TPM, so ignore that state for the fake
310        # RMA mode check.
311        self.confirm_ccd_is_in_factory_mode(check_tpm=False)
312
313        self.host.reboot()
314
315        # Run RMA disable to reset the capabilities.
316        self.rma_ap(disable=True)
317
318        self.confirm_ccd_is_reset()
319
320
321    def rate_limit_check(self, rma_func1, rma_func2):
322        """Verify that Cr50 ratelimits challenge generation from any interface
323
324        Get the challenge from rma_func1. Try to generate a challenge with
325        rma_func2 in a time less than challenge_interval. Make sure it fails.
326        Wait a little bit longer and make sure the function then succeeds.
327
328        Args:
329            rma_func1: the method to generate the first challenge
330            rma_func2: the method to generate the second challenge
331        """
332        time.sleep(self.CHALLENGE_INTERVAL)
333        rma_func1()
334
335        # Wait too short of a time. Verify challenge generation fails
336        time.sleep(self.CHALLENGE_INTERVAL - self.SHORT_WAIT)
337        rma_func2(expected_exit_status=self.UPDATE_ERROR)
338
339        # Wait long enough for the timeout to have elapsed. Verify another
340        # challenge is generated.
341        time.sleep(self.SHORT_WAIT)
342        rma_func2()
343
344
345    def old_authcodes_are_invalid(self, rma_func1, rma_func2):
346        """Verify a response for a previous challenge can't be used again
347
348        Generate 2 challenges. Verify only the authcode from the second
349        challenge can be used to open the device.
350
351        Args:
352            rma_func1: the method to generate the first challenge
353            rma_func2: the method to generate the second challenge
354        """
355        time.sleep(self.CHALLENGE_INTERVAL)
356        old_challenge = rma_func1()
357
358        time.sleep(self.CHALLENGE_INTERVAL)
359        active_challenge = rma_func2()
360
361        invalid_authcode = self.generate_response(old_challenge)[0]
362        valid_authcode = self.generate_response(active_challenge)[0]
363
364        # Use the old authcode
365        rma_func1(invalid_authcode, expected_exit_status=self.UPDATE_ERROR)
366        # make sure factory mode is still disabled
367        self.confirm_ccd_is_reset()
368
369        # Use the authcode generated with the most recent challenge.
370        rma_func1(valid_authcode)
371        # Make sure factory mode has been enabled now that the test has used the
372        # correct authcode.
373        self.confirm_ccd_is_in_factory_mode()
374
375        # Reboot the AP to reenable the TPM
376        self.host.reboot()
377
378        self.rma_ap(disable=True)
379
380        # Verify rma disable disabled factory mode
381        self.confirm_ccd_is_reset()
382
383
384    def verify_interface_combinations(self, test_func):
385        """Run through tests using ap and cli
386
387        Cr50 can run RMA commands from the AP or command line. Test sending
388        commands from both, so we know there aren't any weird interactions
389        between the two.
390
391        Args:
392            test_func: The function to verify some RMA behavior
393        """
394        for rma_interface1 in self.CMD_INTERFACES:
395            rma_func1 = getattr(self, 'rma_' + rma_interface1)
396            for rma_interface2 in self.CMD_INTERFACES:
397                rma_func2 = getattr(self, 'rma_' + rma_interface2)
398                test_func(rma_func1, rma_func2)
399
400
401    def run_once(self):
402        """Verify Cr50 RMA behavior"""
403        self.verify_basic_factory_disable()
404
405        self.verify_interface_combinations(self.rate_limit_check)
406
407        self.verify_interface_combinations(self.rma_open)
408
409        # We can only do RMA unlock with test keys, so this won't be useful
410        # to run unless the Cr50 image is using test keys.
411        if not self.prod_rma_key:
412            self.verify_interface_combinations(self.old_authcodes_are_invalid)
413