Fix normal/recovery mode, and RO firmware vs. RW firmware behavior.

Review URL: http://codereview.chromium.org/2792009
diff --git a/vboot_firmware/lib/rollback_index.c b/vboot_firmware/lib/rollback_index.c
index 3d1553c..b09ea64 100644
--- a/vboot_firmware/lib/rollback_index.c
+++ b/vboot_firmware/lib/rollback_index.c
@@ -34,6 +34,10 @@
   return TPM_SUCCESS;
 }
 
+/* When the return value is TPM_SUCCESS, this function sets *|initialized| to 1
+ * if the spaces have been fully initialized, to 0 if not.  Otherwise
+ * *|initialized| is not changed.
+ */
 static uint32_t GetSpacesInitialized(int* initialized) {
   uint32_t space_holder;
   uint32_t result;
@@ -51,6 +55,8 @@
   return result;
 }
 
+/* Creates the NVRAM spaces, and sets their initial values as needed.
+ */
 static uint32_t InitializeSpaces(void) {
   uint32_t zero = 0;
   uint32_t firmware_perm = TPM_NV_PER_GLOBALLOCK | TPM_NV_PER_PPWRITE;
@@ -67,9 +73,8 @@
   RETURN_ON_FAILURE(InitializeKernelVersionsSpaces());
 
   /* The space KERNEL_VERSIONS_BACKUP_NV_INDEX is used to protect the kernel
-   * versions when entering recovery mode.  The content of space
-   * KERNEL_MUST_USE_BACKUP determines whether the backup value (1) or the
-   * regular value (0) should be trusted.
+   * versions.  The content of space KERNEL_MUST_USE_BACKUP determines whether
+   * only the backup value should be trusted.
    */
   RETURN_ON_FAILURE(TlclDefineSpace(KERNEL_VERSIONS_BACKUP_NV_INDEX,
                                     firmware_perm, sizeof(uint32_t)));
@@ -79,6 +84,10 @@
                                     firmware_perm, sizeof(uint32_t)));
   RETURN_ON_FAILURE(TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
                               (uint8_t*) &zero, sizeof(uint32_t)));
+  RETURN_ON_FAILURE(TlclDefineSpace(DEVELOPER_MODE_NV_INDEX,
+                                    firmware_perm, sizeof(uint32_t)));
+  RETURN_ON_FAILURE(TlclWrite(DEVELOPER_MODE_NV_INDEX,
+                              (uint8_t*) &zero, sizeof(uint32_t)));
 
   /* The space TPM_IS_INITIALIZED_NV_INDEX is used to indicate that the TPM
    * initialization has completed.  Without it we cannot be sure that the last
@@ -90,68 +99,18 @@
   return TPM_SUCCESS;
 }
 
-/* Enters the recovery mode.  If |unlocked| is true, there is some problem with
- * the TPM, so do not attempt to do any more TPM operations, and particularly
- * do not set bGlobalLock.
- */
-void EnterRecovery(int unlocked) {
-  uint32_t combined_versions;
-  uint32_t backup_versions;
+static uint32_t SetDistrustKernelSpaceAtNextBoot(uint32_t distrust) {
   uint32_t must_use_backup;
-  uint32_t result;
-
-  if (!unlocked) {
-    /* Saves the kernel versions and indicates that we should trust the saved
-     * ones.
-     */
-    if (TlclRead(KERNEL_VERSIONS_NV_INDEX, (uint8_t*) &combined_versions,
-                 sizeof(uint32_t)) != TPM_SUCCESS)
-      goto recovery_mode;
-    if (TlclRead(KERNEL_VERSIONS_BACKUP_NV_INDEX, (uint8_t*) &backup_versions,
-                 sizeof(uint32_t)) != TPM_SUCCESS)
-      goto recovery_mode;
-    /* Avoids idempotent writes. */
-    if (combined_versions != backup_versions) {
-      result = TlclWrite(KERNEL_VERSIONS_BACKUP_NV_INDEX,
-                         (uint8_t*) &combined_versions, sizeof(uint32_t));
-      if (result == TPM_E_MAXNVWRITES) {
-        goto forceclear_and_reboot;
-      } else if (result != TPM_SUCCESS) {
-        goto recovery_mode;
-      }
-    }
-
-    if (TlclRead(KERNEL_MUST_USE_BACKUP_NV_INDEX, (uint8_t*) &must_use_backup,
-                 sizeof(uint32_t)) != TPM_SUCCESS)
-      goto recovery_mode;
-    if (must_use_backup != 1) {
-      must_use_backup = 1;
-      result = TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
-                         (uint8_t*) &must_use_backup, sizeof(uint32_t));
-      if (result == TPM_E_MAXNVWRITES) {
-        goto forceclear_and_reboot;
-      } else if (result != TPM_SUCCESS) {
-        goto recovery_mode;
-      }
-    }
-    /* Protects the firmware and backup kernel versions. */
-    if (LockFirmwareVersions() != TPM_SUCCESS)
-      goto recovery_mode;
+  RETURN_ON_FAILURE(TlclRead(KERNEL_MUST_USE_BACKUP_NV_INDEX,
+                             (uint8_t*) &must_use_backup, sizeof(uint32_t)));
+  if (must_use_backup != distrust) {
+     RETURN_ON_FAILURE(TlclWrite(KERNEL_MUST_USE_BACKUP_NV_INDEX,
+                                 (uint8_t*) &distrust, sizeof(uint32_t)));
   }
-
- recovery_mode:
-  debug("entering recovery mode");
-
-  /* TODO(nelson): code for entering recovery mode. */
-
- forceclear_and_reboot:
-  if (TlclForceClear() != TPM_SUCCESS) {
-    goto recovery_mode;
-  }
-  /* TODO: reboot */
+  return TPM_SUCCESS;
 }
 
-static uint32_t GetTPMRollbackIndices(void) {
+static uint32_t GetTPMRollbackIndices(int type) {
   uint32_t firmware_versions;
   uint32_t kernel_versions;
 
@@ -159,17 +118,22 @@
    * rollback index locations are missing or somehow messed up.  We let the
    * caller deal with that.
    */
-  RETURN_ON_FAILURE(TlclRead(FIRMWARE_VERSIONS_NV_INDEX,
-                             (uint8_t*) &firmware_versions,
-                             sizeof(firmware_versions)));
-  RETURN_ON_FAILURE(TlclRead(KERNEL_VERSIONS_NV_INDEX,
-                             (uint8_t*) &kernel_versions,
-                             sizeof(kernel_versions)));
-
-  g_firmware_key_version = firmware_versions >> 16;
-  g_firmware_version = firmware_versions && 0xffff;
-  g_kernel_key_version = kernel_versions >> 16;
-  g_kernel_version = kernel_versions && 0xffff;
+  switch (type) {
+  case FIRMWARE_VERSIONS:
+    RETURN_ON_FAILURE(TlclRead(FIRMWARE_VERSIONS_NV_INDEX,
+                               (uint8_t*) &firmware_versions,
+                               sizeof(firmware_versions)));
+    g_firmware_key_version = firmware_versions >> 16;
+    g_firmware_version = firmware_versions && 0xffff;
+    break;
+  case KERNEL_VERSIONS:
+    RETURN_ON_FAILURE(TlclRead(KERNEL_VERSIONS_NV_INDEX,
+                               (uint8_t*) &kernel_versions,
+                               sizeof(kernel_versions)));
+    g_kernel_key_version = kernel_versions >> 16;
+    g_kernel_version = kernel_versions && 0xffff;
+    break;
+  }
 
   return TPM_SUCCESS;
 }
@@ -241,7 +205,31 @@
   return TPM_SUCCESS;
 }
 
-static uint32_t SetupTPM_(void) {
+/* Checks for transitions between protected mode to developer mode.  When going
+ * into developer mode, clear the TPM.
+ */
+static uint32_t CheckDeveloperModeTransition(uint32_t current_developer) {
+  uint32_t past_developer;
+  int must_clear;
+  RETURN_ON_FAILURE(TlclRead(DEVELOPER_MODE_NV_INDEX,
+                             (uint8_t*) &past_developer,
+                             sizeof(past_developer)));
+  must_clear = current_developer && !past_developer;
+  if (must_clear) {
+    RETURN_ON_FAILURE(TlclForceClear());
+  }
+  if (past_developer != current_developer) {
+    /* (Unauthorized) writes to the TPM succeed even when the TPM is disabled
+     * and deactivated.
+     */
+    RETURN_ON_FAILURE(TlclWrite(DEVELOPER_MODE_NV_INDEX,
+                                (uint8_t*) &current_developer,
+                                sizeof(current_developer)));
+  }
+  return must_clear ? TPM_E_MUST_REBOOT : TPM_SUCCESS;
+}
+
+static uint32_t SetupTPM_(int mode, int developer_flag) {
   uint8_t disable;
   uint8_t deactivated;
   TlclLibInit();
@@ -253,11 +241,11 @@
   if (disable || deactivated) {
     RETURN_ON_FAILURE(TlclSetEnable());
     RETURN_ON_FAILURE(TlclSetDeactivated(0));
-    /* TODO: Reboot */
-    return 9999;
+    return TPM_E_MUST_REBOOT;
   }
-  /* We expect this to fail the first time we run on a device, indicating that
-   * the TPM has not been initialized yet. */
+  /* We expect this to fail the first time we run on a device, because the TPM
+   * has not been initialized yet.
+   */
   if (RecoverKernelSpace() != TPM_SUCCESS) {
     int initialized = 0;
     RETURN_ON_FAILURE(GetSpacesInitialized(&initialized));
@@ -269,32 +257,73 @@
     }
   }
   RETURN_ON_FAILURE(BackupKernelSpace());
-  RETURN_ON_FAILURE(GetTPMRollbackIndices());
+  RETURN_ON_FAILURE(SetDistrustKernelSpaceAtNextBoot(mode == RO_RECOVERY_MODE));
+  RETURN_ON_FAILURE(GetTPMRollbackIndices(FIRMWARE_VERSIONS));
+  RETURN_ON_FAILURE(GetTPMRollbackIndices(KERNEL_VERSIONS));
 
+  RETURN_ON_FAILURE(CheckDeveloperModeTransition(developer_flag));
+
+  /* As a courtesy (I hope) to the caller, lock the firmware versions if we are
+   * in recovery mode.  The normal mode may need to update the firmware
+   * versions, so they cannot be locked here.
+   */
+  if (mode == RO_RECOVERY_MODE) {
+    RETURN_ON_FAILURE(LockFirmwareVersions());
+  }
   return TPM_SUCCESS;
 }
 
-uint32_t SetupTPM(void) {
-  uint32_t result = SetupTPM_();
-  if (result == TPM_E_MAXNVWRITES) {
-    /* ForceClears and reboots */
-    RETURN_ON_FAILURE(TlclForceClear());
-    /* TODO: reboot */
-    return 9999;
-  } else {
-    return result;
+/* SetupTPM starts the TPM and establishes the root of trust for the
+ * anti-rollback mechanism.  SetupTPM can fail for three reasons.  1 A bug. 2 a
+ * TPM hardware failure. 3 An unexpected TPM state due to some attack.  In
+ * general we cannot easily distinguish the kind of failure, so our strategy is
+ * to reboot in recovery mode in all cases.  The recovery mode calls SetupTPM
+ * again, which executes (almost) the same sequence of operations.  There is a
+ * good chance that, if recovery mode was entered because of a TPM failure, the
+ * failure will repeat itself.  (In general this is impossible to guarantee
+ * because we have no way of creating the exact TPM initial state at the
+ * previous boot.)  In recovery mode, we ignore the failure and continue, thus
+ * giving the recovery kernel a chance to fix things (that's why we don't set
+ * bGlobalLock).  The choice is between a knowingly insecure device and a
+ * bricked device.
+ *
+ * As a side note, observe that we go through considerable hoops to avoid using
+ * the STCLEAR permissions for the index spaces.  We do this to avoid writing
+ * to the TPM flashram at every reboot or wake-up, because of concerns about
+ * the durability of the NVRAM.
+ */
+uint32_t SetupTPM(int mode, int developer_flag) {
+  switch (mode) {
+  case RO_RECOVERY_MODE:
+  case RO_NORMAL_MODE: {
+    uint32_t result = SetupTPM_(mode, developer_flag);
+    if (result == TPM_E_MAXNVWRITES) {
+      /* ForceClears and reboots */
+      RETURN_ON_FAILURE(TlclForceClear());
+      return TPM_E_MUST_REBOOT;
+    } else if (mode == RO_NORMAL_MODE) {
+      return result;
+    } else {
+      /* In recovery mode we want to keep going even if there are errors. */
+      return TPM_SUCCESS;
+    }
+  }
+  case RW_NORMAL_MODE:
+    /* There are no TPM writes here, so no need to check for write limit errors.
+     */
+    RETURN_ON_FAILURE(GetTPMRollbackIndices(KERNEL_VERSIONS));
+  default:
+    return TPM_E_INTERNAL_INCONSISTENCY;
   }
 }
 
 uint32_t GetStoredVersions(int type, uint16_t* key_version, uint16_t* version) {
-
-  /* TODO: should verify that SetupTPM() has been called.  Note that
-   * SetupTPM() does hardware setup AND sets global variables.  When we
-   * get down into kernel verification, the hardware setup persists, but
-   * we don't have access to the global variables.  So I guess we DO need
-   * to call SetupTPM() there, and have it be smart enough not to redo the
-   * hardware init, but it still needs to re-read the flags... */
-
+  /* TODO: should verify that SetupTPM() has been called.
+   *
+   * Note that SetupTPM() does hardware setup AND sets global variables.  When
+   * we get down into kernel verification, the hardware setup persists, but we
+   * lose the global variables.
+   */
   switch (type) {
     case FIRMWARE_VERSIONS:
       *key_version = g_firmware_key_version;