blob: 11d20fa8910e79af4b8beb7ac9eaf3386d2261a2 [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
Ruben Rodriguez Buchillon92b39332020-02-20 11:10:49 -080068 if not self.cr50._servo.dts_mode_is_valid():
Mary Ruthvenac1d1472018-03-15 14:52:41 -070069 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
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.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800121 self.prod_rma_key = 'Unsupported' in stdout
122 if self.is_prod_mp and not self.prod_rma_key:
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700123 raise error.TestFail('MP image cannot use test key')
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800124 return re.search('Authcode: +(\S+)', stdout).group(1), self.prod_rma_key
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700125
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)
Mary Ruthvenefa44bb2018-06-11 15:40:40 -0700140 resp = 'rma_auth(.*generated challenge:)?(.*)>'
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700141 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)
Mary Ruthvenefa44bb2018-06-11 15:40:40 -0700146 return (self.parse_challenge(result[0][-1]) if get_challenge else
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700147 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 """
Mary Ruthvenbcec1042018-06-11 11:01:35 -0700164 if disable:
165 cmd = '-a -F disable'
166 else:
167 cmd = '-a -r ' + authcode
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700168 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
Mary Ruthvenbcec1042018-06-11 11:01:35 -0700179 result = cr50_utils.GSCTool(self.host, cmd.split(),
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700180 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
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800197 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')
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800202
203
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700204 def check_ccd_cap_settings(self, rma_opened):
205 """Verify the ccd capability permissions match the RMA state
206
207 Args:
208 rma_opened: True if we expect Cr50 to be RMA opened
209
210 Raises:
211 TestFail if Cr50 is opened when it should be closed or it is closed
212 when it should be opened.
213 """
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700214 time.sleep(self.SHORT_WAIT)
Mary Ruthven885b8032018-08-27 13:56:02 -0700215 caps = self.cr50.get_cap_dict()
216 in_factory_mode, reset = self.cr50.get_cap_overview(caps)
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700217
Mary Ruthven885b8032018-08-27 13:56:02 -0700218 if rma_opened and not in_factory_mode:
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700219 raise error.TestFail('Not all capablities were set to Always')
Mary Ruthven885b8032018-08-27 13:56:02 -0700220 if not rma_opened and not reset:
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700221 raise error.TestFail('Not all capablities were set to Default')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700222
223
224 def rma_open(self, challenge_func, auth_func):
225 """Run the RMA open process
226
227 Run the RMA open process with the given functions. Use challenge func
228 to generate the challenge and auth func to verify the authcode. The
229 commands can be sent from the command line or ap. Both should be able
230 to be used as the challenge or auth function interchangeably.
231
232 Args:
233 challenge_func: The method used to generate the challenge
234 auth_func: The method used to verify the authcode
235 """
236 time.sleep(self.CHALLENGE_INTERVAL)
237
238 # Get the challenge
239 challenge = challenge_func()
240 logging.info(challenge)
241
242 # Try using the challenge. If the Cr50 KeyId is not supported, make sure
243 # RMA open fails.
244 authcode, unsupported_key = self.generate_response(challenge)
245 exit_status = self.UPDATE_ERROR if unsupported_key else self.SUCCESS
246
247 # Attempt RMA open with the given authcode
248 auth_func(authcode=authcode, expected_exit_status=exit_status)
249
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800250 # Make sure ccd is in the proper state. If the RMA key is prod, the test
251 # wont be able to generate the authcode and ccd should still be reset.
252 # It should not be in factory mode.
253 if unsupported_key:
254 self.confirm_ccd_is_reset()
255 else:
256 self.confirm_ccd_is_in_factory_mode()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700257
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800258 self.host.reboot()
259
Namyoon Woo8a1c4aa2019-02-15 14:58:16 -0800260 if not self.tpm_is_responsive():
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800261 raise error.TestFail('TPM was not reenabled after reboot')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700262
263 # Run RMA disable to reset the capabilities.
264 self.rma_ap(disable=True, expected_exit_status=exit_status)
265
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800266 self.confirm_ccd_is_reset()
267
268
Mary Ruthven17a99922019-05-17 11:14:06 -0700269 def confirm_ccd_is_in_factory_mode(self, check_tpm=True):
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800270 """Check wp and capabilities to confirm cr50 is in factory mode"""
271 # The open process takes some time to complete. Wait for it.
272 time.sleep(self.CHALLENGE_INTERVAL)
273
Mary Ruthven17a99922019-05-17 11:14:06 -0700274 if check_tpm and self.tpm_is_responsive():
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800275 raise error.TestFail('TPM was not disabled after RMA open')
276
277 if self.cr50.get_wp_state() != self.WP_PERMANENTLY_DISABLED:
278 raise error.TestFail('HW WP was not disabled after RMA open')
279
280 # Make sure capabilities are all set to Always
281 self.check_ccd_cap_settings(True)
282
283
284 def confirm_ccd_is_reset(self):
285 """Check wp and capabilities to confirm ccd has been reset"""
286 # The open process takes some time to complete. Wait for it.
287 time.sleep(self.CHALLENGE_INTERVAL)
288
Namyoon Woo8a1c4aa2019-02-15 14:58:16 -0800289 if not self.tpm_is_responsive():
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800290 raise error.TestFail('TPM is disabled')
291
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700292 # Confirm write protect has been reset to follow battery presence. The
293 # WP state may be enabled or disabled. The state just can't be forced.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800294 if not self.cr50.wp_is_reset():
295 raise error.TestFail('Factory mode disable did not reset HW WP')
296
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700297 # Make sure capabilities have been reset
298 self.check_ccd_cap_settings(False)
299
300
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800301 def verify_basic_factory_disable(self):
302 """Verify RMA disable works.
303
304 The RMA open process may not be able to be automated, because it
305 requires phyiscal presence and access to the server. This uses console
306 commands to enter the same state as factory mode and then verifies
307 rma disable resets all of that.
308 """
309 self.fake_rma_open()
310
Mary Ruthven17a99922019-05-17 11:14:06 -0700311 # There's no way to disable the TPM, so ignore that state for the fake
312 # RMA mode check.
313 self.confirm_ccd_is_in_factory_mode(check_tpm=False)
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800314
315 self.host.reboot()
316
317 # Run RMA disable to reset the capabilities.
318 self.rma_ap(disable=True)
319
320 self.confirm_ccd_is_reset()
321
322
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700323 def rate_limit_check(self, rma_func1, rma_func2):
324 """Verify that Cr50 ratelimits challenge generation from any interface
325
326 Get the challenge from rma_func1. Try to generate a challenge with
327 rma_func2 in a time less than challenge_interval. Make sure it fails.
328 Wait a little bit longer and make sure the function then succeeds.
329
330 Args:
331 rma_func1: the method to generate the first challenge
332 rma_func2: the method to generate the second challenge
333 """
334 time.sleep(self.CHALLENGE_INTERVAL)
335 rma_func1()
336
337 # Wait too short of a time. Verify challenge generation fails
338 time.sleep(self.CHALLENGE_INTERVAL - self.SHORT_WAIT)
339 rma_func2(expected_exit_status=self.UPDATE_ERROR)
340
341 # Wait long enough for the timeout to have elapsed. Verify another
342 # challenge is generated.
343 time.sleep(self.SHORT_WAIT)
344 rma_func2()
345
346
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800347 def old_authcodes_are_invalid(self, rma_func1, rma_func2):
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700348 """Verify a response for a previous challenge can't be used again
349
350 Generate 2 challenges. Verify only the authcode from the second
351 challenge can be used to open the device.
352
353 Args:
354 rma_func1: the method to generate the first challenge
355 rma_func2: the method to generate the second challenge
356 """
357 time.sleep(self.CHALLENGE_INTERVAL)
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800358 old_challenge = rma_func1()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700359
360 time.sleep(self.CHALLENGE_INTERVAL)
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800361 active_challenge = rma_func2()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700362
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800363 invalid_authcode = self.generate_response(old_challenge)[0]
364 valid_authcode = self.generate_response(active_challenge)[0]
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700365
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800366 # Use the old authcode
367 rma_func1(invalid_authcode, expected_exit_status=self.UPDATE_ERROR)
368 # make sure factory mode is still disabled
369 self.confirm_ccd_is_reset()
370
371 # Use the authcode generated with the most recent challenge.
372 rma_func1(valid_authcode)
373 # Make sure factory mode has been enabled now that the test has used the
374 # correct authcode.
375 self.confirm_ccd_is_in_factory_mode()
376
377 # Reboot the AP to reenable the TPM
378 self.host.reboot()
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700379
380 self.rma_ap(disable=True)
381
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800382 # Verify rma disable disabled factory mode
383 self.confirm_ccd_is_reset()
384
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700385
386 def verify_interface_combinations(self, test_func):
387 """Run through tests using ap and cli
388
389 Cr50 can run RMA commands from the AP or command line. Test sending
390 commands from both, so we know there aren't any weird interactions
391 between the two.
392
393 Args:
394 test_func: The function to verify some RMA behavior
395 """
396 for rma_interface1 in self.CMD_INTERFACES:
397 rma_func1 = getattr(self, 'rma_' + rma_interface1)
398 for rma_interface2 in self.CMD_INTERFACES:
399 rma_func2 = getattr(self, 'rma_' + rma_interface2)
400 test_func(rma_func1, rma_func2)
401
402
403 def run_once(self):
404 """Verify Cr50 RMA behavior"""
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800405 self.verify_basic_factory_disable()
406
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700407 self.verify_interface_combinations(self.rate_limit_check)
408
409 self.verify_interface_combinations(self.rma_open)
410
411 # We can only do RMA unlock with test keys, so this won't be useful
412 # to run unless the Cr50 image is using test keys.
Mary Ruthvend5ce81a2018-11-12 18:01:30 -0800413 if not self.prod_rma_key:
414 self.verify_interface_combinations(self.old_authcodes_are_invalid)