swtpm: If necessary send TPM2_Shutdown() before TPMLIB_Terminate()

If necessary send a TPM2_Shutdown() command to libtpms before processing
CMD_INIT. However, this is only necessary for a TPM 2 and only if the
TPM2_Shutdown command has not been sent by the client (VM TPM driver) as
the last command as it should do under normal circumstances, for example
upon graceful VM shutdown.

This fixes a bug where abrupt VM resets may trigger the TPM 2's dictionary
attack lockout logic due to the TPM 2 not having received a TPM2_Shutdown
command before it was reset using CMD_INIT for example. An OS driver is
typically supposed to send a TPM2_Shutdown to the TPM 2 but an abrupt VM
reset prevents it.

There are 3 control commands where this needs to be done since they
call TPMLIB_Terminate():

- CMD_STOP:
   This command is typically called before setting the state blobs of the
   TPM or before configuring the buffer size [QEMU, test cases].

- CMD_INIT:
   This command is called for resetting and initializing the TPM 2.

- CMD_SHUTDOWN:
   This command is called for a graceful shutdown of the TPM 2.

There are no negative side effects to be expected if TPM2_Shutdown()
is sent before any of these. Also, since none of these are sent before
the state of the TPM is marshalled (for migration for example) migrated
state will not have a TPM2_Shutdown() applied to it (accidentally).

Edk2 sends a sequence of TPM2_Shutdown(SU_STATE) + TPM2_GetRandom()
before suspend-to-ram. Upon wake up a CMD_INIT is sent to the TPM to
reset it, which in this case now requires a TPM2_Shutdown(SU_STATE)
to be sent to the TPM 2 so that certain TPM 2 state is available
again upon resume. To avoid invaliding the SU_STATE, first send a
TPM2_Shutdown(SU_STATE) in *all cases* and only if this fails send a
TPM2_Shutdown(SU_CLEAR). This way the internal state is preserved and
the VM (or user) are expected to use TPM2_Startup(SU_CLEAR) when
staring up the TPM 2 and no previous state needs to be resumed.

Note: The VM's firmware is trusted to use SU_CLEAR under normal circum-
stances and SU_STATE upon resume. So it wouldn't restore the state if
it wasn't needed.

Note: The TPM 2 spec describes the command as follows:

"This command is used to prepare the TPM for a power cycle. The
shutdownType parameter indicates how the subsequent TPM2_Startup() will be
processed.[...]
This command saves TPM state but does not change the state other than the
internal indication that the context has been saved. The TPM shall
continue to accept commands. If a subsequent command changes TPM state
saved by this command, then the effect of this command is nullified. The
TPM MAY nullify this command for any subsequent command rather than check
whether the command changed state saved by this command. If this command
is nullified and if no TPM2_Shutdown() occurs before the next
TPM2_Startup(), then the next TPM2_Startup() shall be
TPM2_Startup(CLEAR)."

Buglink: https://bugzilla.redhat.com/show_bug.cgi?id=2087538
Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
diff --git a/man/man8/swtpm.pod b/man/man8/swtpm.pod
index 7879d7c..605ed8d 100644
--- a/man/man8/swtpm.pod
+++ b/man/man8/swtpm.pod
@@ -406,6 +406,19 @@
 
 =back
 
+=head1 NOTES
+
+If a TPM 2 is used, the user is typically required to send a TPM2_Shutdown()
+command to a TPM 2 to avoid possibly increasing the TPM_PT_LOCKOUT_COUNTER
+that may lead to a dictionary attack (DA) lockout upon next startup
+(TPM2_Startup()) of the TPM 2. Whether the TPM_PT_LOCKOUT_COUNTER is
+increased depends on previous commands sent to the TPM 2 as well as
+internal state of the TPM 2. One example that will trigger the counter to
+increase is the omission of a password when trying to access a
+password-protected object or NVRAM location that has the DA attribute set,
+followed by termination of swtpm without sending TPM2_Shutdown(). To avoid
+a DA lockout swtpm will make a best-effort and send a TPM2_Shutdown(SU_STATE)
+or TPM2_Shutdown(SU_CLEAR) if found necessary.
 
 =head1 SEE ALSO
 
diff --git a/src/swtpm/ctrlchannel.c b/src/swtpm/ctrlchannel.c
index d13353d..babd150 100644
--- a/src/swtpm/ctrlchannel.c
+++ b/src/swtpm/ctrlchannel.c
@@ -554,6 +554,10 @@
         if (n != (ssize_t)sizeof(ptm_init)) /* r/w */
             goto err_bad_input;
 
+        if (*tpm_running)
+            tpmlib_maybe_send_tpm2_shutdown(mlp->tpmversion,
+                                            &mlp->lastCommand);
+
         init_p = (ptm_init *)input.body;
 
         TPMLIB_Terminate();
@@ -576,6 +580,10 @@
         if (n != 0) /* wo */
             goto err_bad_input;
 
+        if (*tpm_running)
+            tpmlib_maybe_send_tpm2_shutdown(mlp->tpmversion,
+                                            &mlp->lastCommand);
+
         TPMLIB_Terminate();
 
         *tpm_running = false;
@@ -588,6 +596,10 @@
         if (n != 0) /* wo */
             goto err_bad_input;
 
+        if (*tpm_running)
+            tpmlib_maybe_send_tpm2_shutdown(mlp->tpmversion,
+                                            &mlp->lastCommand);
+
         TPMLIB_Terminate();
 
         *res_p = htobe32(TPM_SUCCESS);
diff --git a/src/swtpm/cuse_tpm.c b/src/swtpm/cuse_tpm.c
index dcbe018..61bd9a3 100644
--- a/src/swtpm/cuse_tpm.c
+++ b/src/swtpm/cuse_tpm.c
@@ -1091,6 +1091,10 @@
 
             worker_thread_end();
 
+            if (tpm_running)
+                tpmlib_maybe_send_tpm2_shutdown(tpmversion,
+                                                &g_lastCommand);
+
             TPMLIB_Terminate();
 
             tpm_running = false;
@@ -1108,6 +1112,9 @@
     case PTM_STOP:
         worker_thread_end();
 
+        if (tpm_running)
+            tpmlib_maybe_send_tpm2_shutdown(tpmversion, &g_lastCommand);
+
         res = TPM_SUCCESS;
         TPMLIB_Terminate();
 
@@ -1123,6 +1130,9 @@
     case PTM_SHUTDOWN:
         worker_thread_end();
 
+        if (tpm_running)
+            tpmlib_maybe_send_tpm2_shutdown(tpmversion, &g_lastCommand);
+
         res = TPM_SUCCESS;
         TPMLIB_Terminate();
 
diff --git a/src/swtpm/tpmlib.c b/src/swtpm/tpmlib.c
index b29a3d0..e8632f2 100644
--- a/src/swtpm/tpmlib.c
+++ b/src/swtpm/tpmlib.c
@@ -465,3 +465,76 @@
         memcpy(buffer, &ts, tocopy);
     return tocopy;
 }
+
+/*
+ * tpmlib_maybe_send_tpm2_shutdown: Send a TPM2_Shutdown() if necesssary
+ *
+ * @tpmversion: version of TPM
+ * @lastCommand: last command that was sent to the TPM 2 before
+ *               this will be updated if TPM2_Shutdown was sent
+ *
+ * Send a TPM2_Shutdown(SU_STATE) to a TPM 2 if the last-processed command was
+ * not TPM2_Shutdown. If the command fails, send TPM2_Shutdown(SU_CLEAR).
+ */
+void tpmlib_maybe_send_tpm2_shutdown(TPMLIB_TPMVersion tpmversion,
+                                     uint32_t *lastCommand)
+{
+    TPM_RESULT res;
+    unsigned char *rbuffer = NULL;
+    uint32_t rlength = 0;
+    uint32_t rTotal = 0;
+    struct tpm2_shutdown {
+        struct tpm_req_header hdr;
+        uint16_t shutdownType;
+    } tpm2_shutdown = {
+        .hdr = {
+            .tag = htobe16(TPM2_ST_NO_SESSION),
+            .size = htobe32(sizeof(tpm2_shutdown)),
+            .ordinal = htobe32(TPMLIB_TPM2_CC_Shutdown),
+        },
+    };
+    uint16_t shutdownTypes[2] = {
+        TPM2_SU_STATE, TPM2_SU_CLEAR,
+    };
+    struct tpm_resp_header *rsp;
+    uint32_t errcode;
+    size_t i;
+
+    /* Only send TPM2_Shutdown for a TPM 2 and only if TPM2_Shutdown()
+     * was not already sent. Send a TPM2_Shutdown(SU_STATE) first since
+     * this is preserves additional state that will not matter if the
+     * VM starts with TPM2_Startup(SU_CLEAR). Only if this command fails
+     * send TPM2_Shutdown(SU_CLEAR).
+     */
+    if (tpmversion != TPMLIB_TPM_VERSION_2 ||
+        *lastCommand == TPMLIB_TPM2_CC_Shutdown)
+        return;
+
+    for (i = 0; i < ARRAY_LEN(shutdownTypes); i++) {
+#if 0
+        logprintf(STDERR_FILENO,
+                  "Need to send TPM2_Shutdown(%s); previous command: 0x08x\n",
+                  shutdownTypes[i] == TPM2_SU_STATE ? "SU_STATE" : "SU_CLEAR",
+                  *lastCommand);
+#endif
+        tpm2_shutdown.shutdownType = htobe16(shutdownTypes[i]);
+
+        res = TPMLIB_Process(&rbuffer, &rlength, &rTotal,
+                             (unsigned char *)&tpm2_shutdown,
+                             sizeof(tpm2_shutdown));
+
+        if (res || rlength < sizeof(struct tpm_resp_header))
+            continue;
+        rsp = (struct tpm_resp_header *)rbuffer;
+
+        errcode = be32toh(rsp->errcode);
+        if (errcode == TPM_SUCCESS) {
+            *lastCommand = TPMLIB_TPM2_CC_Shutdown;
+            break;
+        } else if (errcode == TPM_RC_INITIALIZE) {
+            /* TPM not initialized by TPM2_Startup - won't work */
+            break;
+        }
+    }
+    free(rbuffer);
+}
diff --git a/src/swtpm/tpmlib.h b/src/swtpm/tpmlib.h
index 2f018cf..cc21b8b 100644
--- a/src/swtpm/tpmlib.h
+++ b/src/swtpm/tpmlib.h
@@ -80,6 +80,9 @@
                                    unsigned char *buffer,
                                    uint32_t buffersize);
 
+void tpmlib_maybe_send_tpm2_shutdown(TPMLIB_TPMVersion tpmversion,
+                                     uint32_t *lastCommand);
+
 struct tpm_req_header {
     uint16_t tag;
     uint32_t size;
@@ -131,12 +134,14 @@
 
 /* TPM 2 error codes */
 #define TPM_RC_INSUFFICIENT 0x09a
+#define TPM_RC_INITIALIZE   0x100
 #define TPM_RC_FAILURE      0x101
 #define TPM_RC_LOCALITY     0x107
 
 /* TPM 2 commands */
 #define TPMLIB_TPM2_CC_CreatePrimary   0x00000131
 #define TPMLIB_TPM2_CC_Startup         0x00000144
+#define TPMLIB_TPM2_CC_Shutdown        0x00000145
 #define TPMLIB_TPM2_CC_Create          0x00000153
 
 /* TPM 2 startup types */