blob: 8ed55d9f716a7b5022d377847d83be84f31bb62e [file] [log] [blame]
Mary Ruthvenac1d1472018-03-15 14:52:41 -07001# 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
Mary Ruthven012f3572018-04-25 10:55:43 -070011from autotest_lib.server.cros.faft.cr50_test import Cr50Test
Mary Ruthvenac1d1472018-03-15 14:52:41 -070012
13
Mary Ruthven012f3572018-04-25 10:55:43 -070014class firmware_Cr50RMAOpen(Cr50Test):
Mary Ruthvenac1d1472018-03-15 14:52:41 -070015 """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
Mary Ruthvend5ce81a2018-11-12 18:01:30 -080031 # Tuple representing WP state when it is force disabled
32 WP_PERMANENTLY_DISABLED = (False, False, False, False)
33
Mary Ruthvenac1d1472018-03-15 14:52:41 -070034 # 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'
Mary Ruthvenefa44bb2018-06-11 15:40:40 -070037 # 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+>)'
Mary Ruthvenac1d1472018-03-15 14:52:41 -070041 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
Mary Ruthvenbf101b92018-06-28 13:52:03 -070055 def initialize(self, host, cmdline_args, full_args):
Mary Ruthvenac1d1472018-03-15 14:52:41 -070056 """Initialize the test"""
Mary Ruthvenbf101b92018-06-28 13:52:03 -070057 super(firmware_Cr50RMAOpen, self).initialize(host, cmdline_args,
58 full_args)
Mary Ruthvenac1d1472018-03-15 14:52:41 -070059 self.host = host
Mary Ruthvenac1d1472018-03-15 14:52:41 -070060
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
Mary Ruthven603a6612018-05-31 15:24:57 -070075 # Disable all capabilities at the start of the test. Go ahead and enable
76 # testlab mode if it isn't enabled.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -080077 self.fast_open(enable_testlab=True)
78 self.cr50.send_command('ccd reset')
79 self.cr50.set_ccd_level('lock')
Mary Ruthvenc898aa72018-06-11 11:05:17 -070080 # 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')
Mary Ruthven0ee13672018-05-25 13:54:59 -070085
Mary Ruthvenac1d1472018-03-15 14:52:41 -070086 self.is_prod_mp = self.get_prod_mp_status()
Mary Ruthvenac1d1472018-03-15 14:52:41 -070087
88
Mary Ruthvend5ce81a2018-11-12 18:01:30 -080089 def tpm_is_enabled(self):
90 """TPM is disabled if the tpm version cant be retrieved"""
91 return not self.host.run('tpm_version', ignore_status=True).exit_status
92
93
Mary Ruthvenac1d1472018-03-15 14:52:41 -070094 def get_prod_mp_status(self):
95 """Returns True if Cr50 is running a prod signed mp flagged image"""
96 # Determine if the running image is using premp flags
97 bid = self.cr50.get_active_board_id_str()
98 premp_flags = int(bid.split(':')[2], 16) & 0x10 if bid else False
99
100 # Check if the running image is signed with prod keys
101 prod_keys = self.cr50.using_prod_rw_keys()
102 logging.info('%s keys with %s flags', 'prod' if prod_keys else 'dev',
103 'premp' if premp_flags else 'mp')
104 return not premp_flags and prod_keys
105
106
107 def parse_challenge(self, challenge):
108 """Remove the whitespace from the challenge"""
109 return re.sub('\s', '', challenge.strip())
110
111
112 def generate_response(self, challenge):
113 """Generate the authcode from the challenge.
114
115 Args:
116 challenge: The Cr50 challenge string
117
118 Returns:
119 A tuple of the authcode and a bool True if the response should
120 work False if it shouldn't
121 """
122 stdout = self.host.run('rma_reset -c ' + challenge).stdout
123 logging.info(stdout)
124 # rma_reset generates authcodes with the test key. MP images should use
125 # prod keys. Make sure prod signed MP images aren't using the test key.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800126 self.prod_rma_key = 'Unsupported' in stdout
127 if self.is_prod_mp and not self.prod_rma_key:
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700128 raise error.TestFail('MP image cannot use test key')
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800129 return re.search('Authcode: +(\S+)', stdout).group(1), self.prod_rma_key
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700130
131
132 def rma_cli(self, authcode='', disable=False, expected_exit_status=SUCCESS):
133 """Run RMA commands using the command line.
134
135 Args:
136 authcode: The authcode string
137 disable: True if RMA open should be disabled.
138 expected_exit_status: the expected exit status
139
140 Returns:
141 The entire stdout from the command or the RMA challenge
142 """
143 cmd = 'rma_auth ' + ('disable' if disable else authcode)
144 get_challenge = not (authcode or disable)
Mary Ruthvenefa44bb2018-06-11 15:40:40 -0700145 resp = 'rma_auth(.*generated challenge:)?(.*)>'
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700146 if expected_exit_status:
147 resp = self.LIMIT_CLI if get_challenge else self.MISMATCH_CLI
148
149 result = self.cr50.send_command_get_output(cmd, [resp])
150 logging.info(result)
Mary Ruthvenefa44bb2018-06-11 15:40:40 -0700151 return (self.parse_challenge(result[0][-1]) if get_challenge else
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700152 result[0])
153
154
155 def rma_ap(self, authcode='', disable=False, expected_exit_status=SUCCESS):
156 """Run RMA commands using vendor commands from the ap.
157
158 Args:
159 authcode: the authcode string.
160 disable: True if RMA open should be disabled.
161 expected_exit_status: the expected exit status
162
163 Returns:
164 The entire stdout from the command or the RMA challenge
165
166 Raises:
167 error.TestFail if there is an unexpected gsctool response
168 """
Mary Ruthvenbcec1042018-06-11 11:01:35 -0700169 if disable:
170 cmd = '-a -F disable'
171 else:
172 cmd = '-a -r ' + authcode
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700173 get_challenge = not (authcode or disable)
174
175 expected_stderr = ''
176 if expected_exit_status:
177 if authcode:
178 expected_stderr = self.MISMATCH_AP
179 elif disable:
180 expected_stderr = self.ERR_DISABLE_AP
181 else:
182 expected_stderr = self.LIMIT_AP
183
Mary Ruthvenbcec1042018-06-11 11:01:35 -0700184 result = cr50_utils.GSCTool(self.host, cmd.split(),
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700185 ignore_status=expected_stderr)
186 logging.info(result)
187 # Various connection issues result in warnings. If there is a real issue
188 # the expected_exit_status will raise it. Ignore any warning messages in
189 # stderr.
190 ignore_stderr = 'WARNING' in result.stderr and not expected_stderr
191 if not ignore_stderr and expected_stderr not in result.stderr.strip():
192 raise error.TestFail('Unexpected stderr: expected %s got %s' %
193 (expected_stderr, result.stderr.strip()))
194 if result.exit_status != expected_exit_status:
195 raise error.TestFail('Unexpected exit_status: expected %s got %s' %
196 (expected_exit_status, result.exit_status))
197 if get_challenge:
198 return self.parse_challenge(result.stdout.split('Challenge:')[-1])
199 return result.stdout
200
201
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800202 def fake_rma_open(self):
203 """Use individual commands to enter the same state as factory mode"""
204 self.cr50.send_command('ccd testlab open')
205 self.cr50.send_command('ccd reset factory')
206 self.cr50.send_command('wp disable atboot')
207 # TODO(b/119626285): Change the command to use --tpm_mode instead of -m
208 # once --tpm_mode can process the 'disable' arg correctly.
209 cr50_utils.GSCTool(self.host, ['gsctool', '--any', '-m', 'disable'])
210
211
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700212 def check_ccd_cap_settings(self, rma_opened):
213 """Verify the ccd capability permissions match the RMA state
214
215 Args:
216 rma_opened: True if we expect Cr50 to be RMA opened
217
218 Raises:
219 TestFail if Cr50 is opened when it should be closed or it is closed
220 when it should be opened.
221 """
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700222 time.sleep(self.SHORT_WAIT)
Mary Ruthven885b8032018-08-27 13:56:02 -0700223 caps = self.cr50.get_cap_dict()
224 in_factory_mode, reset = self.cr50.get_cap_overview(caps)
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700225
Mary Ruthven885b8032018-08-27 13:56:02 -0700226 if rma_opened and not in_factory_mode:
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700227 raise error.TestFail('Not all capablities were set to Always')
Mary Ruthven885b8032018-08-27 13:56:02 -0700228 if not rma_opened and not reset:
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700229 raise error.TestFail('Not all capablities were set to Default')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700230
231
232 def rma_open(self, challenge_func, auth_func):
233 """Run the RMA open process
234
235 Run the RMA open process with the given functions. Use challenge func
236 to generate the challenge and auth func to verify the authcode. The
237 commands can be sent from the command line or ap. Both should be able
238 to be used as the challenge or auth function interchangeably.
239
240 Args:
241 challenge_func: The method used to generate the challenge
242 auth_func: The method used to verify the authcode
243 """
244 time.sleep(self.CHALLENGE_INTERVAL)
245
246 # Get the challenge
247 challenge = challenge_func()
248 logging.info(challenge)
249
250 # Try using the challenge. If the Cr50 KeyId is not supported, make sure
251 # RMA open fails.
252 authcode, unsupported_key = self.generate_response(challenge)
253 exit_status = self.UPDATE_ERROR if unsupported_key else self.SUCCESS
254
255 # Attempt RMA open with the given authcode
256 auth_func(authcode=authcode, expected_exit_status=exit_status)
257
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800258 # Make sure ccd is in the proper state. If the RMA key is prod, the test
259 # wont be able to generate the authcode and ccd should still be reset.
260 # It should not be in factory mode.
261 if unsupported_key:
262 self.confirm_ccd_is_reset()
263 else:
264 self.confirm_ccd_is_in_factory_mode()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700265
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800266 self.host.reboot()
267
268 if not self.tpm_is_enabled():
269 raise error.TestFail('TPM was not reenabled after reboot')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700270
271 # Run RMA disable to reset the capabilities.
272 self.rma_ap(disable=True, expected_exit_status=exit_status)
273
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800274 self.confirm_ccd_is_reset()
275
276
277 def confirm_ccd_is_in_factory_mode(self):
278 """Check wp and capabilities to confirm cr50 is in factory mode"""
279 # The open process takes some time to complete. Wait for it.
280 time.sleep(self.CHALLENGE_INTERVAL)
281
282 if self.tpm_is_enabled():
283 raise error.TestFail('TPM was not disabled after RMA open')
284
285 if self.cr50.get_wp_state() != self.WP_PERMANENTLY_DISABLED:
286 raise error.TestFail('HW WP was not disabled after RMA open')
287
288 # Make sure capabilities are all set to Always
289 self.check_ccd_cap_settings(True)
290
291
292 def confirm_ccd_is_reset(self):
293 """Check wp and capabilities to confirm ccd has been reset"""
294 # The open process takes some time to complete. Wait for it.
295 time.sleep(self.CHALLENGE_INTERVAL)
296
297 if not self.tpm_is_enabled():
298 raise error.TestFail('TPM is disabled')
299
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700300 # Confirm write protect has been reset to follow battery presence. The
301 # WP state may be enabled or disabled. The state just can't be forced.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800302 if not self.cr50.wp_is_reset():
303 raise error.TestFail('Factory mode disable did not reset HW WP')
304
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700305 # Make sure capabilities have been reset
306 self.check_ccd_cap_settings(False)
307
308
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800309 def verify_basic_factory_disable(self):
310 """Verify RMA disable works.
311
312 The RMA open process may not be able to be automated, because it
313 requires phyiscal presence and access to the server. This uses console
314 commands to enter the same state as factory mode and then verifies
315 rma disable resets all of that.
316 """
317 self.fake_rma_open()
318
319 self.confirm_ccd_is_in_factory_mode()
320
321 self.host.reboot()
322
323 # Run RMA disable to reset the capabilities.
324 self.rma_ap(disable=True)
325
326 self.confirm_ccd_is_reset()
327
328
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700329 def rate_limit_check(self, rma_func1, rma_func2):
330 """Verify that Cr50 ratelimits challenge generation from any interface
331
332 Get the challenge from rma_func1. Try to generate a challenge with
333 rma_func2 in a time less than challenge_interval. Make sure it fails.
334 Wait a little bit longer and make sure the function then succeeds.
335
336 Args:
337 rma_func1: the method to generate the first challenge
338 rma_func2: the method to generate the second challenge
339 """
340 time.sleep(self.CHALLENGE_INTERVAL)
341 rma_func1()
342
343 # Wait too short of a time. Verify challenge generation fails
344 time.sleep(self.CHALLENGE_INTERVAL - self.SHORT_WAIT)
345 rma_func2(expected_exit_status=self.UPDATE_ERROR)
346
347 # Wait long enough for the timeout to have elapsed. Verify another
348 # challenge is generated.
349 time.sleep(self.SHORT_WAIT)
350 rma_func2()
351
352
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800353 def old_authcodes_are_invalid(self, rma_func1, rma_func2):
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700354 """Verify a response for a previous challenge can't be used again
355
356 Generate 2 challenges. Verify only the authcode from the second
357 challenge can be used to open the device.
358
359 Args:
360 rma_func1: the method to generate the first challenge
361 rma_func2: the method to generate the second challenge
362 """
363 time.sleep(self.CHALLENGE_INTERVAL)
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800364 old_challenge = rma_func1()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700365
366 time.sleep(self.CHALLENGE_INTERVAL)
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800367 active_challenge = rma_func2()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700368
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800369 invalid_authcode = self.generate_response(old_challenge)[0]
370 valid_authcode = self.generate_response(active_challenge)[0]
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700371
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800372 # Use the old authcode
373 rma_func1(invalid_authcode, expected_exit_status=self.UPDATE_ERROR)
374 # make sure factory mode is still disabled
375 self.confirm_ccd_is_reset()
376
377 # Use the authcode generated with the most recent challenge.
378 rma_func1(valid_authcode)
379 # Make sure factory mode has been enabled now that the test has used the
380 # correct authcode.
381 self.confirm_ccd_is_in_factory_mode()
382
383 # Reboot the AP to reenable the TPM
384 self.host.reboot()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700385
386 self.rma_ap(disable=True)
387
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800388 # Verify rma disable disabled factory mode
389 self.confirm_ccd_is_reset()
390
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700391
392 def verify_interface_combinations(self, test_func):
393 """Run through tests using ap and cli
394
395 Cr50 can run RMA commands from the AP or command line. Test sending
396 commands from both, so we know there aren't any weird interactions
397 between the two.
398
399 Args:
400 test_func: The function to verify some RMA behavior
401 """
402 for rma_interface1 in self.CMD_INTERFACES:
403 rma_func1 = getattr(self, 'rma_' + rma_interface1)
404 for rma_interface2 in self.CMD_INTERFACES:
405 rma_func2 = getattr(self, 'rma_' + rma_interface2)
406 test_func(rma_func1, rma_func2)
407
408
409 def run_once(self):
410 """Verify Cr50 RMA behavior"""
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800411 self.verify_basic_factory_disable()
412
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700413 self.verify_interface_combinations(self.rate_limit_check)
414
415 self.verify_interface_combinations(self.rma_open)
416
417 # We can only do RMA unlock with test keys, so this won't be useful
418 # to run unless the Cr50 image is using test keys.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800419 if not self.prod_rma_key:
420 self.verify_interface_combinations(self.old_authcodes_are_invalid)