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