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