blob: eecaac05b052bcdd50c1c38808bfa4f2494f0497 [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
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
Mary Ruthvenac1d1472018-03-15 14:52:41 -070053
54 if not hasattr(self, 'cr50'):
55 raise error.TestNAError('Test can only be run on devices with '
56 'access to the Cr50 console')
57
58 if not self.cr50.has_command('rma_auth'):
59 raise error.TestNAError('Cannot test on Cr50 without RMA support')
60
61 if not self.cr50.using_servo_v4():
62 raise error.TestNAError('This messes with ccd settings. Use flex '
63 'cable to run the test.')
64
65 if self.host.run('rma_reset -h', ignore_status=True).exit_status == 127:
66 raise error.TestNAError('Cannot test RMA open without rma_reset')
67
Mary Ruthven603a6612018-05-31 15:24:57 -070068 # Disable all capabilities at the start of the test. Go ahead and enable
69 # testlab mode if it isn't enabled.
70 self.cr50.fast_open(enable_testlab=True)
71 self.cr50.send_command('ccd reset')
72 self.cr50.set_ccd_level('lock')
Mary Ruthven0ee13672018-05-25 13:54:59 -070073 self.check_ccd_cap_settings(False)
74
Mary Ruthvenac1d1472018-03-15 14:52:41 -070075 self.is_prod_mp = self.get_prod_mp_status()
Mary Ruthvenac1d1472018-03-15 14:52:41 -070076
77
78 def get_prod_mp_status(self):
79 """Returns True if Cr50 is running a prod signed mp flagged image"""
80 # Determine if the running image is using premp flags
81 bid = self.cr50.get_active_board_id_str()
82 premp_flags = int(bid.split(':')[2], 16) & 0x10 if bid else False
83
84 # Check if the running image is signed with prod keys
85 prod_keys = self.cr50.using_prod_rw_keys()
86 logging.info('%s keys with %s flags', 'prod' if prod_keys else 'dev',
87 'premp' if premp_flags else 'mp')
88 return not premp_flags and prod_keys
89
90
91 def parse_challenge(self, challenge):
92 """Remove the whitespace from the challenge"""
93 return re.sub('\s', '', challenge.strip())
94
95
96 def generate_response(self, challenge):
97 """Generate the authcode from the challenge.
98
99 Args:
100 challenge: The Cr50 challenge string
101
102 Returns:
103 A tuple of the authcode and a bool True if the response should
104 work False if it shouldn't
105 """
106 stdout = self.host.run('rma_reset -c ' + challenge).stdout
107 logging.info(stdout)
108 # rma_reset generates authcodes with the test key. MP images should use
109 # prod keys. Make sure prod signed MP images aren't using the test key.
110 self.prod_keyid = 'Unsupported KeyID' in stdout
111 if self.is_prod_mp and not self.prod_keyid:
112 raise error.TestFail('MP image cannot use test key')
113 return re.search('Authcode: +(\S+)', stdout).group(1), self.prod_keyid
114
115
116 def rma_cli(self, authcode='', disable=False, expected_exit_status=SUCCESS):
117 """Run RMA commands using the command line.
118
119 Args:
120 authcode: The authcode string
121 disable: True if RMA open should be disabled.
122 expected_exit_status: the expected exit status
123
124 Returns:
125 The entire stdout from the command or the RMA challenge
126 """
127 cmd = 'rma_auth ' + ('disable' if disable else authcode)
128 get_challenge = not (authcode or disable)
129 resp = 'rma_auth(.*)>'
130 if expected_exit_status:
131 resp = self.LIMIT_CLI if get_challenge else self.MISMATCH_CLI
132
133 result = self.cr50.send_command_get_output(cmd, [resp])
134 logging.info(result)
135 return (self.parse_challenge(result[0][1]) if get_challenge else
136 result[0])
137
138
139 def rma_ap(self, authcode='', disable=False, expected_exit_status=SUCCESS):
140 """Run RMA commands using vendor commands from the ap.
141
142 Args:
143 authcode: the authcode string.
144 disable: True if RMA open should be disabled.
145 expected_exit_status: the expected exit status
146
147 Returns:
148 The entire stdout from the command or the RMA challenge
149
150 Raises:
151 error.TestFail if there is an unexpected gsctool response
152 """
153 cmd = 'disable' if disable else authcode
154 get_challenge = not (authcode or disable)
155
156 expected_stderr = ''
157 if expected_exit_status:
158 if authcode:
159 expected_stderr = self.MISMATCH_AP
160 elif disable:
161 expected_stderr = self.ERR_DISABLE_AP
162 else:
163 expected_stderr = self.LIMIT_AP
164
165 result = cr50_utils.RMAOpen(self.host, cmd,
166 ignore_status=expected_stderr)
167 logging.info(result)
168 # Various connection issues result in warnings. If there is a real issue
169 # the expected_exit_status will raise it. Ignore any warning messages in
170 # stderr.
171 ignore_stderr = 'WARNING' in result.stderr and not expected_stderr
172 if not ignore_stderr and expected_stderr not in result.stderr.strip():
173 raise error.TestFail('Unexpected stderr: expected %s got %s' %
174 (expected_stderr, result.stderr.strip()))
175 if result.exit_status != expected_exit_status:
176 raise error.TestFail('Unexpected exit_status: expected %s got %s' %
177 (expected_exit_status, result.exit_status))
178 if get_challenge:
179 return self.parse_challenge(result.stdout.split('Challenge:')[-1])
180 return result.stdout
181
182
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700183 def check_ccd_cap_settings(self, rma_opened):
184 """Verify the ccd capability permissions match the RMA state
185
186 Args:
187 rma_opened: True if we expect Cr50 to be RMA opened
188
189 Raises:
190 TestFail if Cr50 is opened when it should be closed or it is closed
191 when it should be opened.
192 """
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700193 time.sleep(self.SHORT_WAIT)
194 caps = self.cr50.get_cap_dict().values()
195 closed = len(caps) == caps.count('Default')
196 opened = len(caps) == caps.count('Always')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700197
Mary Ruthvenac171ca2018-05-22 17:14:35 -0700198 if rma_opened and not opened:
199 raise error.TestFail('Not all capablities were set to Always')
200 if not rma_opened and not closed:
201 raise error.TestFail('Not all capablities were set to Default')
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700202
203
204 def rma_open(self, challenge_func, auth_func):
205 """Run the RMA open process
206
207 Run the RMA open process with the given functions. Use challenge func
208 to generate the challenge and auth func to verify the authcode. The
209 commands can be sent from the command line or ap. Both should be able
210 to be used as the challenge or auth function interchangeably.
211
212 Args:
213 challenge_func: The method used to generate the challenge
214 auth_func: The method used to verify the authcode
215 """
216 time.sleep(self.CHALLENGE_INTERVAL)
217
218 # Get the challenge
219 challenge = challenge_func()
220 logging.info(challenge)
221
222 # Try using the challenge. If the Cr50 KeyId is not supported, make sure
223 # RMA open fails.
224 authcode, unsupported_key = self.generate_response(challenge)
225 exit_status = self.UPDATE_ERROR if unsupported_key else self.SUCCESS
226
227 # Attempt RMA open with the given authcode
228 auth_func(authcode=authcode, expected_exit_status=exit_status)
229
230 # Make sure capabilities are in the correct state.
231 self.check_ccd_cap_settings(not unsupported_key)
232
233 # Force enable write protect, so we can make sure it's reset after RMA
234 # disable
235 if not unsupported_key:
236 logging.info(self.cr50.send_command_get_output('wp enable',
Mary Ruthvenafe51ef2018-05-25 17:58:55 -0700237 ['wp.*>']))
Mary Ruthvenac1d1472018-03-15 14:52:41 -0700238
239 # Run RMA disable to reset the capabilities.
240 self.rma_ap(disable=True, expected_exit_status=exit_status)
241
242 # Confirm write protect has been reset to follow battery presence. The
243 # WP state may be enabled or disabled. The state just can't be forced.
244 logging.info(self.cr50.send_command_get_output('wp',
245 ['Flash WP: (enabled|disabled)']))
246 # Make sure capabilities have been reset
247 self.check_ccd_cap_settings(False)
248
249
250 def rate_limit_check(self, rma_func1, rma_func2):
251 """Verify that Cr50 ratelimits challenge generation from any interface
252
253 Get the challenge from rma_func1. Try to generate a challenge with
254 rma_func2 in a time less than challenge_interval. Make sure it fails.
255 Wait a little bit longer and make sure the function then succeeds.
256
257 Args:
258 rma_func1: the method to generate the first challenge
259 rma_func2: the method to generate the second challenge
260 """
261 time.sleep(self.CHALLENGE_INTERVAL)
262 rma_func1()
263
264 # Wait too short of a time. Verify challenge generation fails
265 time.sleep(self.CHALLENGE_INTERVAL - self.SHORT_WAIT)
266 rma_func2(expected_exit_status=self.UPDATE_ERROR)
267
268 # Wait long enough for the timeout to have elapsed. Verify another
269 # challenge is generated.
270 time.sleep(self.SHORT_WAIT)
271 rma_func2()
272
273
274 def challenge_reset(self, rma_func1, rma_func2):
275 """Verify a response for a previous challenge can't be used again
276
277 Generate 2 challenges. Verify only the authcode from the second
278 challenge can be used to open the device.
279
280 Args:
281 rma_func1: the method to generate the first challenge
282 rma_func2: the method to generate the second challenge
283 """
284 time.sleep(self.CHALLENGE_INTERVAL)
285 challenge1 = rma_func1()
286 authcode1 = self.generate_response(challenge1)[0]
287
288 time.sleep(self.CHALLENGE_INTERVAL)
289 challenge2 = rma_func2()
290 authcode2 = self.generate_response(challenge2)[0]
291
292 rma_func1(authcode1, expected_exit_status=self.UPDATE_ERROR)
293 rma_func1(authcode2)
294
295 time.sleep(self.SHORT_WAIT)
296
297 self.rma_ap(disable=True)
298
299
300 def verify_interface_combinations(self, test_func):
301 """Run through tests using ap and cli
302
303 Cr50 can run RMA commands from the AP or command line. Test sending
304 commands from both, so we know there aren't any weird interactions
305 between the two.
306
307 Args:
308 test_func: The function to verify some RMA behavior
309 """
310 for rma_interface1 in self.CMD_INTERFACES:
311 rma_func1 = getattr(self, 'rma_' + rma_interface1)
312 for rma_interface2 in self.CMD_INTERFACES:
313 rma_func2 = getattr(self, 'rma_' + rma_interface2)
314 test_func(rma_func1, rma_func2)
315
316
317 def run_once(self):
318 """Verify Cr50 RMA behavior"""
319 self.verify_interface_combinations(self.rate_limit_check)
320
321 self.verify_interface_combinations(self.rma_open)
322
323 # We can only do RMA unlock with test keys, so this won't be useful
324 # to run unless the Cr50 image is using test keys.
325 if not self.prod_keyid:
326 self.verify_interface_combinations(self.challenge_reset)