1# Copyright 2017 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 argparse
6import logging
7import re
8
9from autotest_lib.client.common_lib import error
10
11
12RO = 'ro'
13RW = 'rw'
14BID = 'bid'
15CR50_PROD = '/opt/google/cr50/firmware/cr50.bin.prod'
16CR50_PREPVT = '/opt/google/cr50/firmware/cr50.bin.prepvt'
17CR50_STATE = '/var/cache/cr50*'
18CR50_VERSION = '/var/cache/cr50-version'
19GET_CR50_VERSION = 'cat %s' % CR50_VERSION
20GET_CR50_MESSAGES ='grep "cr50-.*\[" /var/log/messages'
21UPDATE_FAILURE = 'unexpected cr50-update exit code'
22DUMMY_VER = '-1.-1.-1'
23# This dictionary is used to search the gsctool output for the version strings.
24# There are two gsctool commands that will return versions: 'fwver' and
25# 'binvers'.
26#
27# 'fwver'   is used to get the running RO and RW versions from cr50
28# 'binvers'  gets the version strings for each RO and RW region in the given
29#            file
30#
31# The value in the dictionary is the regular expression that can be used to
32# find the version strings for each region.
33#
34# --fwver
35#   example output:
36#           open_device 18d1:5014
37#           found interface 3 endpoint 4, chunk_len 64
38#           READY
39#           -------
40#           start
41#           target running protocol version 6
42#           keyids: RO 0xaa66150f, RW 0xde88588d
43#           offsets: backup RO at 0x40000, backup RW at 0x44000
44#           Current versions:
45#           RO 0.0.10
46#           RW 0.0.21
47#   match groupdict:
48#           {
49#               'ro': '0.0.10',
50#               'rw': '0.0.21'
51#           }
52#
53# --binvers
54#   example output:
55#           read 524288(0x80000) bytes from /tmp/cr50.bin
56#           RO_A:0.0.10 RW_A:0.0.21[00000000:00000000:00000000]
57#           RO_B:0.0.10 RW_B:0.0.21[00000000:00000000:00000000]
58#   match groupdict:
59#           {
60#               'rw_b': '0.0.21',
61#               'rw_a': '0.0.21',
62#               'ro_b': '0.0.10',
63#               'ro_a': '0.0.10',
64#               'bid_a': '00000000:00000000:00000000',
65#               'bid_b': '00000000:00000000:00000000'
66#           }
67VERSION_RE = {
68    '--fwver' : '\nRO (?P<ro>\S+).*\nRW (?P<rw>\S+)',
69    '--binvers' : 'RO_A:(?P<ro_a>[\d\.]+).*' \
70           'RW_A:(?P<rw_a>[\d\.]+)(\[(?P<bid_a>[\d\:A-z]+)\])?.*' \
71           'RO_B:(?P<ro_b>\S+).*' \
72           'RW_B:(?P<rw_b>[\d\.]+)(\[(?P<bid_b>[\d\:A-z]+)\])?.*',
73}
74UPDATE_TIMEOUT = 60
75UPDATE_OK = 1
76
77MP_BID_FLAGS = 0x7f80
78ERASED_BID_INT = 0xffffffff
79ERASED_BID_STR = hex(ERASED_BID_INT)
80# With an erased bid, the flags and board id will both be erased
81ERASED_CHIP_BID = (ERASED_BID_INT, ERASED_BID_INT, ERASED_BID_INT)
82# Any image with this board id will run on any device
83EMPTY_IMAGE_BID = '00000000:00000000:00000000'
84EMPTY_IMAGE_BID_CHARACTERS = set(EMPTY_IMAGE_BID)
85SYMBOLIC_BID_LENGTH = 4
86
87gsctool = argparse.ArgumentParser()
88gsctool.add_argument('-a', '--any', dest='universal', action='store_true')
89# use /dev/tpm0 to send the command
90gsctool.add_argument('-s', '--systemdev', dest='systemdev', action='store_true')
91gsctool.add_argument('-o', '--ccd_open', dest='ccd_open', action='store_true')
92# Any command used for something other than updating. These commands should
93# never timeout because they do not force cr50 to reboot. They should all just
94# return information about cr50 and should only have a nonzero exit status if
95# something went wrong.
96gsctool.add_argument('-b', '--binvers', '-f', '--fwver', '-g', '--getbootmode',
97                     '-i', '--board_id', '-r', '--rma_auth', '-F', '--factory',
98                     '-m', '--tpm_mode', '-L', '--flog',
99                     dest='info_cmd', action='store_true')
100# upstart and post_reset will post resets instead of rebooting immediately
101gsctool.add_argument('-u', '--upstart', '-p', '--post_reset', dest='post_reset',
102                     action='store_true')
103gsctool.add_argument('extras', nargs=argparse.REMAINDER)
104
105
106def AssertVersionsAreEqual(name_a, ver_a, name_b, ver_b):
107    """Raise an error ver_a isn't the same as ver_b
108
109    Args:
110        name_a: the name of section a
111        ver_a: the version string for section a
112        name_b: the name of section b
113        ver_b: the version string for section b
114
115    Raises:
116        AssertionError if ver_a is not equal to ver_b
117    """
118    assert ver_a == ver_b, ('Versions do not match: %s %s %s %s' %
119                            (name_a, ver_a, name_b, ver_b))
120
121
122def GetNewestVersion(ver_a, ver_b):
123    """Compare the versions. Return the newest one. If they are the same return
124    None."""
125    a = [int(x) for x in ver_a.split('.')]
126    b = [int(x) for x in ver_b.split('.')]
127
128    if a > b:
129        return ver_a
130    if b > a:
131        return ver_b
132    return None
133
134
135def GetVersion(versions, name):
136    """Return the version string from the dictionary.
137
138    Get the version for each key in the versions dictionary that contains the
139    substring name. Make sure all of the versions match and return the version
140    string. Raise an error if the versions don't match.
141
142    Args:
143        version: dictionary with the partition names as keys and the
144                 partition version strings as values.
145        name: the string used to find the relevant items in versions.
146
147    Returns:
148        the version from versions or "-1.-1.-1" if an invalid RO was detected.
149    """
150    ver = None
151    key = None
152    for k, v in versions.iteritems():
153        if name in k:
154            if v == DUMMY_VER:
155                logging.info('Detected invalid %s %s', name, v)
156                return v
157            elif ver:
158                AssertVersionsAreEqual(key, ver, k, v)
159            else:
160                ver = v
161                key = k
162    return ver
163
164
165def FindVersion(output, arg):
166    """Find the ro and rw versions.
167
168    Args:
169        output: The string to search
170        arg: string representing the gsctool option, either '--binvers' or
171             '--fwver'
172
173    Returns:
174        a tuple of the ro and rw versions
175    """
176    versions = re.search(VERSION_RE[arg], output)
177    if not versions:
178        raise Exception('Unable to determine version from: %s' % output)
179
180    versions = versions.groupdict()
181    ro = GetVersion(versions, RO)
182    rw = GetVersion(versions, RW)
183    # --binver is the only gsctool command that may have bid keys in its
184    # versions dictionary. If no bid keys exist, bid will be None.
185    bid = GetVersion(versions, BID)
186    # Use GetBoardIdInfoString to standardize all board ids to the non
187    # symbolic form.
188    return ro, rw, GetBoardIdInfoString(bid, symbolic=False)
189
190
191def GetSavedVersion(client):
192    """Return the saved version from /var/cache/cr50-version
193
194    Some boards dont have cr50.bin.prepvt. They may still have prepvt flags.
195    It is possible that cr50-update wont successfully run in this case.
196    Return None if the file doesn't exist.
197
198    Returns:
199        the version saved in cr50-version or None if cr50-version doesn't exist
200    """
201    if not client.path_exists(CR50_VERSION):
202        return None
203
204    result = client.run(GET_CR50_VERSION).stdout.strip()
205    return FindVersion(result, '--fwver')
206
207
208def StopTrunksd(client):
209    """Stop trunksd on the client"""
210    if 'running' in client.run('status trunksd').stdout:
211        client.run('stop trunksd')
212
213
214def GSCTool(client, args, ignore_status=False):
215    """Run gsctool with the given args.
216
217    Args:
218        client: the object to run commands on
219        args: a list of strings that contiain the gsctool args
220
221    Returns:
222        the result of gsctool
223    """
224    options = gsctool.parse_args(args)
225
226    if options.systemdev:
227        StopTrunksd(client)
228
229    # If we are updating the cr50 image, gsctool will return a non-zero exit
230    # status so we should ignore it.
231    ignore_status = not options.info_cmd or ignore_status
232    # immediate reboots are only honored if the command is sent using /dev/tpm0
233    expect_reboot = ((options.systemdev or options.universal) and
234            not options.post_reset and not options.info_cmd)
235
236    result = client.run('gsctool %s' % ' '.join(args),
237                        ignore_status=ignore_status,
238                        ignore_timeout=expect_reboot,
239                        timeout=UPDATE_TIMEOUT)
240
241    # After a posted reboot, the gsctool exit code should equal 1.
242    if (result and result.exit_status and result.exit_status != UPDATE_OK and
243        not ignore_status):
244        logging.debug(result)
245        raise error.TestFail('Unexpected gsctool exit code after %s %d' %
246                             (' '.join(args), result.exit_status))
247    return result
248
249
250def GetVersionFromUpdater(client, args):
251    """Return the version from gsctool"""
252    result = GSCTool(client, args).stdout.strip()
253    return FindVersion(result, args[0])
254
255
256def GetFwVersion(client):
257    """Get the running version using 'gsctool --fwver'"""
258    return GetVersionFromUpdater(client, ['--fwver', '-a'])
259
260
261def GetBinVersion(client, image=CR50_PROD):
262    """Get the image version using 'gsctool --binvers image'"""
263    return GetVersionFromUpdater(client, ['--binvers', image])
264
265
266def GetVersionString(ver):
267    """Combine the RO and RW tuple into a understandable string"""
268    return 'RO %s RW %s%s' % (ver[0], ver[1],
269           ' BID %s' % ver[2] if ver[2] else '')
270
271
272def GetRunningVersion(client):
273    """Get the running Cr50 version.
274
275    The version from gsctool and /var/cache/cr50-version should be the
276    same. Get both versions and make sure they match.
277
278    Args:
279        client: the object to run commands on
280
281    Returns:
282        running_ver: a tuple with the ro and rw version strings
283
284    Raises:
285        TestFail
286        - If the version in /var/cache/cr50-version is not the same as the
287          version from 'gsctool --fwver'
288    """
289    running_ver = GetFwVersion(client)
290    saved_ver = GetSavedVersion(client)
291
292    if saved_ver:
293        AssertVersionsAreEqual('Running', GetVersionString(running_ver),
294                               'Saved', GetVersionString(saved_ver))
295    return running_ver
296
297
298def GetActiveCr50ImagePath(client):
299    """Get the path the device uses to update cr50
300
301    Extract the active cr50 path from the cr50-update messages. This path is
302    determined by cr50-get-name based on the board id flag value.
303
304    Args:
305        client: the object to run commands on
306
307    Raises:
308        TestFail
309            - If cr50-update uses more than one path or if the path we find
310              is not a known cr50 update path.
311    """
312    ClearUpdateStateAndReboot(client)
313    messages = client.run(GET_CR50_MESSAGES).stdout.strip()
314    paths = set(re.findall('/opt/google/cr50/firmware/cr50.bin[\S]+', messages))
315    if not paths:
316        raise error.TestFail('Could not determine cr50-update path')
317    path = paths.pop()
318    if len(paths) > 1 or (path != CR50_PROD and path != CR50_PREPVT):
319        raise error.TestFail('cannot determine cr50 path')
320    return path
321
322
323def CheckForFailures(client, last_message):
324    """Check for any unexpected cr50-update exit codes.
325
326    This only checks the cr50 update messages that have happened since
327    last_message. If a unexpected exit code is detected it will raise an error>
328
329    Args:
330        client: the object to run commands on
331        last_message: the last cr50 message from the last update run
332
333    Returns:
334        the last cr50 message in /var/log/messages
335
336    Raises:
337        TestFail
338            - If there is a unexpected cr50-update exit code after last_message
339              in /var/log/messages
340    """
341    messages = client.run(GET_CR50_MESSAGES).stdout.strip()
342    if last_message:
343        messages = messages.rsplit(last_message, 1)[-1].split('\n')
344        failures = []
345        for message in messages:
346            if UPDATE_FAILURE in message:
347                failures.append(message)
348        if len(failures):
349            logging.info(messages)
350            raise error.TestFail('Detected unexpected exit code during update: '
351                                 '%s' % failures)
352    return messages[-1]
353
354
355def VerifyUpdate(client, ver='', last_message=''):
356    """Verify that the saved update state is correct and there were no
357    unexpected cr50-update exit codes since the last update.
358
359    Args:
360        client: the object to run commands on
361        ver: the expected version tuple (ro ver, rw ver)
362        last_message: the last cr50 message from the last update run
363
364    Returns:
365        new_ver: a tuple containing the running ro and rw versions
366        last_message: The last cr50 update message in /var/log/messages
367    """
368    # Check that there were no unexpected reboots from cr50-result
369    last_message = CheckForFailures(client, last_message)
370    logging.debug('last cr50 message %s', last_message)
371
372    new_ver = GetRunningVersion(client)
373    if ver != '':
374        if DUMMY_VER != ver[0]:
375            AssertVersionsAreEqual('Old RO', ver[0], 'Updated RO', new_ver[0])
376        AssertVersionsAreEqual('Old RW', ver[1], 'Updated RW', new_ver[1])
377    return new_ver, last_message
378
379
380def GetDevicePath(ext):
381    """Return the device path for the .prod or .prepvt image."""
382    if ext == 'prod':
383        return CR50_PROD
384    elif ext == 'prepvt':
385        return CR50_PREPVT
386    raise error.TestError('Unsupported cr50 image type %r' % ext)
387
388
389def ClearUpdateStateAndReboot(client):
390    """Removes the cr50 status files in /var/cache and reboots the AP"""
391    # If any /var/cache/cr50* files exist, remove them.
392    result = client.run('ls %s' % CR50_STATE, ignore_status=True)
393    if not result.exit_status:
394        client.run('rm %s' % ' '.join(result.stdout.split()))
395    elif result.exit_status != 2:
396        # Exit status 2 means the file didn't exist. If the command fails for
397        # some other reason, raise an error.
398        logging.debug(result)
399        raise error.TestFail(result.stderr)
400    client.reboot()
401
402
403def InstallImage(client, src, dest=CR50_PROD):
404    """Copy the image at src to dest on the dut
405
406    Args:
407        client: the object to run commands on
408        src: the image location of the server
409        dest: the desired location on the dut
410
411    Returns:
412        The filename where the image was copied to on the dut, a tuple
413        containing the RO and RW version of the file
414    """
415    # Send the file to the DUT
416    client.send_file(src, dest)
417
418    ver = GetBinVersion(client, dest)
419    client.run('sync')
420    return dest, ver
421
422
423def GetBoardIdInfoTuple(board_id_str):
424    """Convert the string into board id args.
425
426    Split the board id string board_id:(mask|board_id_inv):flags to a tuple of
427    its parts. Each element will be converted to an integer.
428
429    Returns:
430        board id int, mask|board_id_inv, and flags or None if its a universal
431        image.
432    """
433    # In tests None is used for universal board ids. Some old images don't
434    # support getting board id, so we use None. Convert 0:0:0 to None.
435    if not board_id_str or set(board_id_str) == EMPTY_IMAGE_BID_CHARACTERS:
436        return None
437
438    board_id, param2, flags = board_id_str.split(':')
439    return GetIntBoardId(board_id), int(param2, 16), int(flags, 16)
440
441
442def GetBoardIdInfoString(board_id_info, symbolic=False):
443    """Convert the board id list or str into a symbolic or non symbolic str.
444
445    This can be used to convert the board id info list into a symbolic or non
446    symbolic board id string. It can also be used to convert a the board id
447    string into a board id string with a symbolic or non symbolic board id
448
449    Args:
450        board_id_info: A string of the form board_id:(mask|board_id_inv):flags
451                       or a list with the board_id, (mask|board_id_inv), flags
452
453    Returns:
454        (board_id|symbolic_board_id):(mask|board_id_inv):flags. Will return
455        None if if the given board id info is empty or is not valid
456    """
457    # Convert board_id_info to a tuple if it's a string.
458    if isinstance(board_id_info, str):
459        board_id_info = GetBoardIdInfoTuple(board_id_info)
460
461    if not board_id_info:
462        return None
463
464    board_id, param2, flags = board_id_info
465    # Get the hex string for board id
466    board_id = '%08x' % GetIntBoardId(board_id)
467
468    # Convert the board id hex to a symbolic board id
469    if symbolic:
470        board_id = GetSymbolicBoardId(board_id)
471
472    # Return the board_id_str:8_digit_hex_mask: 8_digit_hex_flags
473    return '%s:%08x:%08x' % (board_id, param2, flags)
474
475
476def GetSymbolicBoardId(board_id):
477    """Convert an integer board id to a symbolic string
478
479    Args:
480        board_id: the board id to convert to the symbolic board id
481
482    Returns:
483        the 4 character symbolic board id
484    """
485    symbolic_board_id = ''
486    board_id = GetIntBoardId(board_id)
487
488    # Convert the int to a symbolic board id
489    for i in range(SYMBOLIC_BID_LENGTH):
490        symbolic_board_id += chr((board_id >> (i * 8)) & 0xff)
491    symbolic_board_id = symbolic_board_id[::-1]
492
493    # Verify the created board id is 4 characters
494    if len(symbolic_board_id) != SYMBOLIC_BID_LENGTH:
495        raise error.TestFail('Created invalid symbolic board id %s' %
496                             symbolic_board_id)
497    return symbolic_board_id
498
499
500def ConvertSymbolicBoardId(symbolic_board_id):
501    """Convert the symbolic board id str to an int
502
503    Args:
504        symbolic_board_id: a ASCII string. It can be up to 4 characters
505
506    Returns:
507        the symbolic board id string converted to an int
508    """
509    board_id = 0
510    for c in symbolic_board_id:
511        board_id = ord(c) | (board_id << 8)
512    return board_id
513
514
515def GetIntBoardId(board_id):
516    """"Return the gsctool interpretation of board_id
517
518    Args:
519        board_id: a int or string value of the board id
520
521    Returns:
522        a int representation of the board id
523    """
524    if type(board_id) == int:
525        return board_id
526
527    if len(board_id) <= SYMBOLIC_BID_LENGTH:
528        return ConvertSymbolicBoardId(board_id)
529
530    return int(board_id, 16)
531
532
533def GetExpectedFlags(flags):
534    """If flags are not specified, gsctool will set them to 0xff00
535
536    Args:
537        flags: The int value or None
538
539    Returns:
540        the original flags or 0xff00 if flags is None
541    """
542    return flags if flags != None else 0xff00
543
544
545def RMAOpen(client, cmd='', ignore_status=False):
546    """Run gsctool RMA commands"""
547    return GSCTool(client, ['-a', '-r', cmd], ignore_status)
548
549
550def GetChipBoardId(client):
551    """Return the board id and flags
552
553    Args:
554        client: the object to run commands on
555
556    Returns:
557        a tuple with the int values of board id, board id inv, flags
558
559    Raises:
560        TestFail if the second board id response field is not ~board_id
561    """
562    result = GSCTool(client, ['-a', '-i']).stdout.strip()
563    board_id_info = result.split('Board ID space: ')[-1].strip().split(':')
564    board_id, board_id_inv, flags = [int(val, 16) for val in board_id_info]
565    logging.info('BOARD_ID: %x:%x:%x', board_id, board_id_inv, flags)
566
567    if board_id == board_id_inv == ERASED_BID_INT:
568        if flags == ERASED_BID_INT:
569            logging.info('board id is erased')
570        else:
571            logging.info('board id type is erased')
572    elif board_id & board_id_inv:
573        raise error.TestFail('board_id_inv should be ~board_id got %x %x' %
574                             (board_id, board_id_inv))
575    return board_id, board_id_inv, flags
576
577
578def GetChipBIDFromImageBID(image_bid, brand):
579    """Calculate a chip bid that will work with the image bid.
580
581    Returns:
582        A tuple of integers (bid type, ~bid type, bid flags)
583    """
584    image_bid_tuple = GetBoardIdInfoTuple(image_bid)
585    # GetBoardIdInfoTuple returns None if the image isn't board id locked.
586    # Generate a Tuple of all 0s the rest of the function can use.
587    if not image_bid_tuple:
588        image_bid_tuple = (0, 0, 0)
589
590    image_bid, image_mask, image_flags = image_bid_tuple
591    if image_mask:
592        new_brand = GetSymbolicBoardId(image_bid)
593    else:
594        new_brand = brand
595    new_flags = image_flags or MP_BID_FLAGS
596    bid_type = GetIntBoardId(new_brand)
597    # If the board id type is erased, type_inv should also be unset.
598    if bid_type == ERASED_BID_INT:
599        return (ERASED_BID_INT, ERASED_BID_INT, new_flags)
600    return bid_type, 0xffffffff & ~bid_type, new_flags
601
602
603def CheckChipBoardId(client, board_id, flags, board_id_inv=None):
604    """Compare the given board_id and flags to the running board_id and flags
605
606    Interpret board_id and flags how gsctool would interpret them, then compare
607    those interpreted values to the running board_id and flags.
608
609    Args:
610        client: the object to run commands on
611        board_id: a hex str, symbolic str, or int value for board_id
612        board_id_inv: a hex str or int value of board_id_inv. Ignore
613                      board_id_inv if None. board_id_inv is ~board_id unless
614                      the board id is erased. In case both should be 0xffffffff.
615        flags: the int value of flags or None
616
617    Raises:
618        TestFail if the new board id info does not match
619    """
620    # Read back the board id and flags
621    new_board_id, new_board_id_inv, new_flags = GetChipBoardId(client)
622
623    expected_board_id = GetIntBoardId(board_id)
624    expected_flags = GetExpectedFlags(flags)
625
626    if board_id_inv == None:
627        new_board_id_inv_str = ''
628        expected_board_id_inv_str = ''
629    else:
630        new_board_id_inv_str = '%08x:' % new_board_id_inv
631        expected_board_id_inv = GetIntBoardId(board_id_inv)
632        expected_board_id_inv_str = '%08x:' % expected_board_id_inv
633
634    expected_str = '%08x:%s%08x' % (expected_board_id,
635                                    expected_board_id_inv_str,
636                                    expected_flags)
637    new_str = '%08x:%s%08x' % (new_board_id, new_board_id_inv_str, new_flags)
638
639    if new_str != expected_str:
640        raise error.TestFail('Failed to set board id: expected %r got %r' %
641                             (expected_str, new_str))
642
643
644def SetChipBoardId(client, board_id, flags=None, pad=True):
645    """Sets the board id and flags
646
647    Args:
648        client: the object to run commands on
649        board_id: a string of the symbolic board id or board id hex value. If
650                  the string is less than 4 characters long it will be
651                  considered a symbolic value
652        flags: a int flag value. If board_id is a symbolic value, then this will
653               be ignored.
654        pad: pad any int board id, so the string is not 4 characters long.
655
656    Raises:
657        TestFail if we were unable to set the flags to the correct value
658    """
659    if isinstance(board_id, int):
660        # gsctool will interpret any 4 character string as a RLZ code. If pad is
661        # true, pad the board id with 0s to make sure the board id isn't 4
662        # characters long.
663        board_id_arg = ('0x%08x' % board_id) if pad else hex(board_id)
664    else:
665        board_id_arg = board_id
666    if flags != None:
667        board_id_arg += ':' + hex(flags)
668    # Set the board id using the given board id and flags
669    result = GSCTool(client, ['-a', '-i', board_id_arg]).stdout.strip()
670
671    CheckChipBoardId(client, board_id, flags)
672
673def DumpFlog(client):
674    """Retrieve contents of the flash log"""
675    return GSCTool(client, ['-a', '-L']).stdout.strip()
676