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