blob: b31c99cab29e2826312852ab6209e1b420da908c [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
11from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
12
13
14class firmware_Cr50RMAOpen(FirmwareTest):
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 # Various Error Messages from the command line and AP RMA failures
32 MISMATCH_CLI = 'Auth code does not match.'
33 MISMATCH_AP = 'rma unlock failed, code 6'
34 LIMIT_CLI = 'RMA Auth error 0x504'
35 LIMIT_AP = 'error 4'
36 ERR_DISABLE_AP = 'error 7'
37 DISABLE_WARNING = ('mux_client_request_session: read from master failed: '
38 'Broken pipe')
39 # GSCTool exit statuses
40 UPDATE_ERROR = 3
41 SUCCESS = 0
42 # Cr50 limits generating challenges to once every 10 seconds
43 CHALLENGE_INTERVAL = 10
44 SHORT_WAIT = 3
45 # Cr50 RMA commands can be sent from the AP or command line. They should
46 # behave the same and be interchangeable
47 CMD_INTERFACES = ['ap', 'cli']
48
49 def initialize(self, host, cmdline_args):
50 """Initialize the test"""
51 super(firmware_Cr50RMAOpen, self).initialize(host, cmdline_args)
52 self.host = host
53 self.restore_state = False
54
55 if not hasattr(self, 'cr50'):
56 raise error.TestNAError('Test can only be run on devices with '
57 'access to the Cr50 console')
58
59 if not self.cr50.has_command('rma_auth'):
60 raise error.TestNAError('Cannot test on Cr50 without RMA support')
61
62 if not self.cr50.using_servo_v4():
63 raise error.TestNAError('This messes with ccd settings. Use flex '
64 'cable to run the test.')
65
66 if self.host.run('rma_reset -h', ignore_status=True).exit_status == 127:
67 raise error.TestNAError('Cannot test RMA open without rma_reset')
68
69 self.is_prod_mp = self.get_prod_mp_status()
70 self.original_settings = self.get_ccd_cap_settings()
71 # Make sure we can't run on devices that are already open. We can't run
72 # on a device that is open, because we dont know the keys, and we may
73 # not be able to reset the state.
74 #
75 # Devices in the ccd testlab should not be running this test, but if
76 # they do, we don't want this to take down the lab. The test lab uses
77 # prod keys, so we can't easily reopen them remotely.
78 if not self.caps_are_restricted(self.original_settings):
79 raise error.TestNAError('Device is already RMA opened.')
80 self.restore_state = True
81
82
83 def cleanup(self):
84 """Make sure RMA mode is disabled"""
85 if (self.restore_state and self.original_settings !=
86 self.get_ccd_cap_settings()):
87 time.sleep(self.CHALLENGE_INTERVAL)
88 self.rma_ap(disable=True)
89 super(firmware_Cr50RMAOpen, self).cleanup()
90
91
92 def get_prod_mp_status(self):
93 """Returns True if Cr50 is running a prod signed mp flagged image"""
94 # Determine if the running image is using premp flags
95 bid = self.cr50.get_active_board_id_str()
96 premp_flags = int(bid.split(':')[2], 16) & 0x10 if bid else False
97
98 # Check if the running image is signed with prod keys
99 prod_keys = self.cr50.using_prod_rw_keys()
100 logging.info('%s keys with %s flags', 'prod' if prod_keys else 'dev',
101 'premp' if premp_flags else 'mp')
102 return not premp_flags and prod_keys
103
104
105 def parse_challenge(self, challenge):
106 """Remove the whitespace from the challenge"""
107 return re.sub('\s', '', challenge.strip())
108
109
110 def generate_response(self, challenge):
111 """Generate the authcode from the challenge.
112
113 Args:
114 challenge: The Cr50 challenge string
115
116 Returns:
117 A tuple of the authcode and a bool True if the response should
118 work False if it shouldn't
119 """
120 stdout = self.host.run('rma_reset -c ' + challenge).stdout
121 logging.info(stdout)
122 # rma_reset generates authcodes with the test key. MP images should use
123 # prod keys. Make sure prod signed MP images aren't using the test key.
124 self.prod_keyid = 'Unsupported KeyID' in stdout
125 if self.is_prod_mp and not self.prod_keyid:
126 raise error.TestFail('MP image cannot use test key')
127 return re.search('Authcode: +(\S+)', stdout).group(1), self.prod_keyid
128
129
130 def rma_cli(self, authcode='', disable=False, expected_exit_status=SUCCESS):
131 """Run RMA commands using the command line.
132
133 Args:
134 authcode: The authcode string
135 disable: True if RMA open should be disabled.
136 expected_exit_status: the expected exit status
137
138 Returns:
139 The entire stdout from the command or the RMA challenge
140 """
141 cmd = 'rma_auth ' + ('disable' if disable else authcode)
142 get_challenge = not (authcode or disable)
143 resp = 'rma_auth(.*)>'
144 if expected_exit_status:
145 resp = self.LIMIT_CLI if get_challenge else self.MISMATCH_CLI
146
147 result = self.cr50.send_command_get_output(cmd, [resp])
148 logging.info(result)
149 return (self.parse_challenge(result[0][1]) if get_challenge else
150 result[0])
151
152
153 def rma_ap(self, authcode='', disable=False, expected_exit_status=SUCCESS):
154 """Run RMA commands using vendor commands from the ap.
155
156 Args:
157 authcode: the authcode string.
158 disable: True if RMA open should be disabled.
159 expected_exit_status: the expected exit status
160
161 Returns:
162 The entire stdout from the command or the RMA challenge
163
164 Raises:
165 error.TestFail if there is an unexpected gsctool response
166 """
167 cmd = 'disable' if disable else 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.RMAOpen(self.host, cmd,
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 get_ccd_cap_settings(self):
198 """Get the current cr50 capability settings"""
199 time.sleep(self.CHALLENGE_INTERVAL)
200 caps = self.cr50.send_command_get_output('ccd',
201 ['Capabilities: \S+\s(.*)Use'])[0][1]
202 logging.info(caps)
203 return caps
204
205
206 def caps_are_restricted(self, caps):
207 """Returns True if some capability permissions are still restricted"""
208 return 'IfOpened' in caps or 'IfUnlocked' in caps
209
210
211 def check_ccd_cap_settings(self, rma_opened):
212 """Verify the ccd capability permissions match the RMA state
213
214 Args:
215 rma_opened: True if we expect Cr50 to be RMA opened
216
217 Raises:
218 TestFail if Cr50 is opened when it should be closed or it is closed
219 when it should be opened.
220 """
221 caps = self.get_ccd_cap_settings()
222 still_closed = self.caps_are_restricted(caps)
223
224 if still_closed and rma_opened:
225 raise error.TestFail('Some capabilities are still restricted')
226 if not rma_opened and caps != self.original_settings:
227 raise error.TestFail('RMA disable failed to reset capabilities')
228
229
230 def rma_open(self, challenge_func, auth_func):
231 """Run the RMA open process
232
233 Run the RMA open process with the given functions. Use challenge func
234 to generate the challenge and auth func to verify the authcode. The
235 commands can be sent from the command line or ap. Both should be able
236 to be used as the challenge or auth function interchangeably.
237
238 Args:
239 challenge_func: The method used to generate the challenge
240 auth_func: The method used to verify the authcode
241 """
242 time.sleep(self.CHALLENGE_INTERVAL)
243
244 # Get the challenge
245 challenge = challenge_func()
246 logging.info(challenge)
247
248 # Try using the challenge. If the Cr50 KeyId is not supported, make sure
249 # RMA open fails.
250 authcode, unsupported_key = self.generate_response(challenge)
251 exit_status = self.UPDATE_ERROR if unsupported_key else self.SUCCESS
252
253 # Attempt RMA open with the given authcode
254 auth_func(authcode=authcode, expected_exit_status=exit_status)
255
256 # Make sure capabilities are in the correct state.
257 self.check_ccd_cap_settings(not unsupported_key)
258
259 # Force enable write protect, so we can make sure it's reset after RMA
260 # disable
261 if not unsupported_key:
262 logging.info(self.cr50.send_command_get_output('wp enable',
263 ['.*>']))
264
265 # Run RMA disable to reset the capabilities.
266 self.rma_ap(disable=True, expected_exit_status=exit_status)
267
268 # Confirm write protect has been reset to follow battery presence. The
269 # WP state may be enabled or disabled. The state just can't be forced.
270 logging.info(self.cr50.send_command_get_output('wp',
271 ['Flash WP: (enabled|disabled)']))
272 # Make sure capabilities have been reset
273 self.check_ccd_cap_settings(False)
274
275
276 def rate_limit_check(self, rma_func1, rma_func2):
277 """Verify that Cr50 ratelimits challenge generation from any interface
278
279 Get the challenge from rma_func1. Try to generate a challenge with
280 rma_func2 in a time less than challenge_interval. Make sure it fails.
281 Wait a little bit longer and make sure the function then succeeds.
282
283 Args:
284 rma_func1: the method to generate the first challenge
285 rma_func2: the method to generate the second challenge
286 """
287 time.sleep(self.CHALLENGE_INTERVAL)
288 rma_func1()
289
290 # Wait too short of a time. Verify challenge generation fails
291 time.sleep(self.CHALLENGE_INTERVAL - self.SHORT_WAIT)
292 rma_func2(expected_exit_status=self.UPDATE_ERROR)
293
294 # Wait long enough for the timeout to have elapsed. Verify another
295 # challenge is generated.
296 time.sleep(self.SHORT_WAIT)
297 rma_func2()
298
299
300 def challenge_reset(self, rma_func1, rma_func2):
301 """Verify a response for a previous challenge can't be used again
302
303 Generate 2 challenges. Verify only the authcode from the second
304 challenge can be used to open the device.
305
306 Args:
307 rma_func1: the method to generate the first challenge
308 rma_func2: the method to generate the second challenge
309 """
310 time.sleep(self.CHALLENGE_INTERVAL)
311 challenge1 = rma_func1()
312 authcode1 = self.generate_response(challenge1)[0]
313
314 time.sleep(self.CHALLENGE_INTERVAL)
315 challenge2 = rma_func2()
316 authcode2 = self.generate_response(challenge2)[0]
317
318 rma_func1(authcode1, expected_exit_status=self.UPDATE_ERROR)
319 rma_func1(authcode2)
320
321 time.sleep(self.SHORT_WAIT)
322
323 self.rma_ap(disable=True)
324
325
326 def verify_interface_combinations(self, test_func):
327 """Run through tests using ap and cli
328
329 Cr50 can run RMA commands from the AP or command line. Test sending
330 commands from both, so we know there aren't any weird interactions
331 between the two.
332
333 Args:
334 test_func: The function to verify some RMA behavior
335 """
336 for rma_interface1 in self.CMD_INTERFACES:
337 rma_func1 = getattr(self, 'rma_' + rma_interface1)
338 for rma_interface2 in self.CMD_INTERFACES:
339 rma_func2 = getattr(self, 'rma_' + rma_interface2)
340 test_func(rma_func1, rma_func2)
341
342
343 def run_once(self):
344 """Verify Cr50 RMA behavior"""
345 self.verify_interface_combinations(self.rate_limit_check)
346
347 self.verify_interface_combinations(self.rma_open)
348
349 # We can only do RMA unlock with test keys, so this won't be useful
350 # to run unless the Cr50 image is using test keys.
351 if not self.prod_keyid:
352 self.verify_interface_combinations(self.challenge_reset)