Home | History | Annotate | Download | only in firmware_Cr50RMAOpen
      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 
      5 import logging
      6 import re
      7 import time
      8 
      9 from autotest_lib.client.common_lib import error
     10 from autotest_lib.client.common_lib.cros import cr50_utils
     11 from autotest_lib.server.cros.faft.cr50_test import Cr50Test
     12 
     13 
     14 class 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