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