autotest: add test to cr50 responds to CCD disable flag

This test will use firmware_SetFWMP to modify the FWMP space. It
verifies that the flags can be set and that Cr50 unlock acts correctly
during each flag state. Eventually tests will be added that test
enterprise enrollment sets the correct flags. This test uses cryptohome
to set the flags.

BUG=b:35587053
BRANCH=none
TEST=run test

Change-Id: Ic83c59a66fd29b5fff3ed5ed472ecf9947ac7140
Signed-off-by: Mary Ruthven <mruthven@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/518550
Reviewed-by: Vadim Bendebury <vbendeb@chromium.org>
diff --git a/client/cros/cryptohome.py b/client/cros/cryptohome.py
index 8cca77c..7feac32 100644
--- a/client/cros/cryptohome.py
+++ b/client/cros/cryptohome.py
@@ -136,6 +136,66 @@
     return status
 
 
+def get_fwmp(cleared_fwmp=False):
+    """Get the firmware management parameters.
+
+    Args:
+        cleared_fwmp: True if the space should not exist.
+
+    Returns:
+        The dictionary with the FWMP contents, for example:
+        { 'flags': 0xbb41,
+          'developer_key_hash':
+            "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\
+             000\000\000\000\000\000\000\000\000\000\000",
+        }
+        or a dictionary with the Error if the FWMP doesn't exist and
+        cleared_fwmp is True
+        { 'error': 'CRYPTOHOME_ERROR_FIRMWARE_MANAGEMENT_PARAMETERS_INVALID' }
+
+    Raises:
+         ChromiumOSError if any expected field is not found in the cryptohome
+         output. This would typically happen when FWMP state does not match
+         'clreared_fwmp'
+    """
+    out = __run_cmd(CRYPTOHOME_CMD +
+                    ' --action=get_firmware_management_parameters')
+
+    if cleared_fwmp:
+        fields = ['error']
+    else:
+        fields = ['flags', 'developer_key_hash']
+
+    status = {}
+    for field in fields:
+        match = re.search('%s: (\S+)\n' % field, out)
+        if not match:
+            raise ChromiumOSError('Invalid FWMP field %s: "%s".' %
+                                  (field, out))
+        status[field] = match.group(1)
+    return status
+
+
+def set_fwmp(flags, developer_key_hash=None):
+    """Set the firmware management parameter contents.
+
+    Args:
+        developer_key_hash: a string with the developer key hash
+
+    Raises:
+         ChromiumOSError cryptohome cannot set the FWMP contents
+    """
+    cmd = (CRYPTOHOME_CMD +
+          ' --action=set_firmware_management_parameters '
+          '--flags=' + flags)
+    if developer_key_hash:
+        cmd += ' --developer_key_hash=' + developer_key_hash
+
+    out = __run_cmd(cmd)
+    if 'SetFirmwareManagementParameters success' not in out:
+        raise ChromiumOSError('failed to set FWMP: %s' % out)
+
+
 def is_tpm_lockout_in_effect():
     """Returns true if the TPM lockout is in effect; false otherwise."""
     status = get_tpm_more_status()
diff --git a/client/site_tests/firmware_SetFWMP/firmware_SetFWMP.py b/client/site_tests/firmware_SetFWMP/firmware_SetFWMP.py
new file mode 100644
index 0000000..bf1a1f5
--- /dev/null
+++ b/client/site_tests/firmware_SetFWMP/firmware_SetFWMP.py
@@ -0,0 +1,47 @@
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import time
+
+from autotest_lib.client.bin import test
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.cros import cryptohome
+
+
+class firmware_SetFWMP(test.test):
+    """Set the FWMP flags and dev_key_hash."""
+    version = 1
+
+    def own_tpm(self):
+        """Own the TPM"""
+        cryptohome.take_tpm_ownership()
+        for i in range(4):
+            status = cryptohome.get_tpm_status()
+            if status['Owned']:
+                return status
+            time.sleep(2)
+        raise error.TestFail('Failed to own the TPM %s' % status)
+
+    def run_once(self, fwmp_cleared=True, flags=None, dev_key_hash=None):
+        # make sure the FMWP is in the expected state
+        cryptohome.get_fwmp(fwmp_cleared)
+        status = cryptohome.get_tpm_status()
+        # Own the TPM
+        if not status['Owned']:
+            status = self.own_tpm()
+
+        # Verify we have access to the password
+        if not status['Password']:
+            logging.warning('No access to the password')
+
+        logging.info(status)
+
+        # Set the FWMP flags using a dev key hash
+        cryptohome.set_fwmp(flags, dev_key_hash)
+
+        # Check that the flags are set
+        fwmp = cryptohome.get_fwmp()
+        if flags and fwmp['flags'] != str(int(flags, 16)):
+            raise error.TestFail('Unexpected FWMP status: %s', fwmp)
diff --git a/server/cros/servo/chrome_cr50.py b/server/cros/servo/chrome_cr50.py
index 0e9cbe0..68de1c4 100644
--- a/server/cros/servo/chrome_cr50.py
+++ b/server/cros/servo/chrome_cr50.py
@@ -34,8 +34,10 @@
     ACTIVE = '\nRW_(A|B): +\* +(%s)(/DBG|)?' % (VERSION_FORMAT)
     WAKE_CHAR = '\n'
     START_UNLOCK_TIMEOUT = 20
-    UNLOCK = ['Unlock sequence starting. Continue until (\S+)']
     GETTIME = ['= (\S+)']
+    UNLOCK = ['Unlock sequence starting. Continue until (\S+)']
+    FWMP_LOCKED_PROD = ["Managed device console can't be unlocked"]
+    FWMP_LOCKED_DBG = ['Ignoring FWMP unlock setting']
 
 
     def __init__(self, servo):
diff --git a/server/site_tests/firmware_FWMPDisableCCD/control b/server/site_tests/firmware_FWMPDisableCCD/control
new file mode 100644
index 0000000..6ab8740
--- /dev/null
+++ b/server/site_tests/firmware_FWMPDisableCCD/control
@@ -0,0 +1,28 @@
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+
+AUTHOR = "mruthven"
+NAME = "firmware_FWMPDisableCCD"
+PURPOSE = "Verify the cr50 response to FWMP flags being set"
+TIME = "MEDIUM"
+ATTRIBUTES = "suite:cr50_stress_experimental"
+TEST_TYPE = "server"
+
+DOC = """
+This test will set FWMP flags. If the test has access to the cr50 console, it
+will verify that cr50 sees the ccd disable flag is set and disables console
+unlock.
+"""
+
+args_dict = utils.args_to_dict(args)
+servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, servo_args=servo_args)
+
+    job.run_test("firmware_FWMPDisableCCD", host=host, cmdline_args=args)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/firmware_FWMPDisableCCD/firmware_FWMPDisableCCD.py b/server/site_tests/firmware_FWMPDisableCCD/firmware_FWMPDisableCCD.py
new file mode 100644
index 0000000..769cde7
--- /dev/null
+++ b/server/site_tests/firmware_FWMPDisableCCD/firmware_FWMPDisableCCD.py
@@ -0,0 +1,128 @@
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import tpm_utils
+from autotest_lib.server import test, autotest
+from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
+
+
+class firmware_FWMPDisableCCD(FirmwareTest):
+    """A test that uses cryptohome to set the FWMP flags and verifies that
+    cr50 disables/enables console unlock."""
+    version = 1
+
+    FWMP_DEV_DISABLE_CCD_UNLOCK = (1 << 6)
+
+
+    def initialize(self, host, cmdline_args):
+        """Initialize servo check if cr50 exists"""
+        super(firmware_FWMPDisableCCD, self).initialize(host, cmdline_args)
+
+        self.host = host
+        self.test_cr50_unlock = hasattr(self, "cr50")
+
+        if self.test_cr50_unlock:
+            rv = self.cr50.send_command_get_output('lock dummy', ['.+>'])
+            if 'Access Denied' in rv[0]:
+                self.test_cr50_unlock = False
+                logging.warning('Cr50 image is permanently locked.')
+
+
+    def cr50_try_unlock(self, fwmp_disabled_unlock):
+        """Run lock disable
+
+        The FWMP flags may disable ccd. If they do then we expect lock disable
+        to fail.
+
+        @param fwmp_disabled_unlock: True if the unlock process should fail
+        """
+        if fwmp_disabled_unlock:
+            if 'DBG' in self.servo.get('cr50_version'):
+                response = self.cr50.FWMP_LOCKED_DBG
+            else:
+                response = self.cr50.FWMP_LOCKED_PROD
+            self.cr50.send_command_get_output('lock disable', response)
+        else:
+            self.cr50.lock_disable()
+
+
+    def cr50_check_fwmp_flag(self, fwmp_disabled_unlock):
+        """Verify cr50 thinks the flag is set or cleared"""
+        response = 'Console unlock%s allowed' % (' not' if fwmp_disabled_unlock
+                                                 else '')
+        self.cr50.send_command_get_output('sysrst pulse', [response])
+
+
+    def cr50_check_lock_control(self, flags):
+        """Verify cr50 lock enable/disable works as intended based on flags.
+
+        If flags & self.FWMP_DEV_DISABLE_CCD_UNLOCK is true, lock disable should
+        fail.
+
+        This will only run during a test with access to the cr50  console
+
+        @param flags: A string with the FWMP settings.
+        """
+        if not self.test_cr50_unlock:
+            return
+
+        fwmp_disabled_unlock = (self.FWMP_DEV_DISABLE_CCD_UNLOCK &
+                               int(flags, 16))
+
+        logging.info('Flags are set to %s ccd unlock is %s', flags,
+                     'disabled' if fwmp_disabled_unlock else 'enabled')
+
+        # Verify that the ccd disable flag is still set
+        self.cr50_check_fwmp_flag(fwmp_disabled_unlock)
+
+        # Enable the lock
+        self.cr50.lock_enable()
+
+        # Try to disable it
+        self.cr50_try_unlock(fwmp_disabled_unlock)
+
+        # Verify that the ccd disable flag is still set
+        self.cr50_check_fwmp_flag(fwmp_disabled_unlock)
+
+
+    def check_fwmp(self, flags, clear_tpm_owner):
+        """Set the flags and verify ccd lock/unlock
+
+        Args:
+            flags: A string to used set the FWMP flags
+            clear_tpm_owner: True if the TPM owner needs to be cleared before
+                             setting the flags and verifying ccd lock/unlock
+        """
+        if clear_tpm_owner:
+            logging.info('Clearing TPM owner')
+            tpm_utils.ClearTPMOwnerRequest(self.host)
+
+        logging.info('setting flags to %s', flags)
+        autotest.Autotest(self.host).run_test('firmware_SetFWMP', flags=flags,
+                fwmp_cleared=clear_tpm_owner, check_client_result=True)
+
+        # Verify ccd lock/unlock with the current flags works as intended.
+        self.cr50_check_lock_control(flags)
+
+
+    def run_once(self):
+        self.check_fwmp('0xaa00', True)
+        # Verify that the flags can be changed on the same boot
+        self.check_fwmp('0xbb00', False)
+
+        # Verify setting FWMP_DEV_DISABLE_CCD_UNLOCK disables ccd
+        self.check_fwmp(hex(self.FWMP_DEV_DISABLE_CCD_UNLOCK), True)
+
+        # 0x41 is the flag setting when dev boot is disabled. Make sure that
+        # nothing unexpected happens.
+        self.check_fwmp('0x41', True)
+
+        # Clear the TPM owner and verify lock can still be enabled/disabled when
+        # the FWMP has not been created
+        tpm_utils.ClearTPMOwnerRequest(self.host)
+        self.cr50_check_lock_control('0')