diff mbox

[v3,2/7] tpm: validate TPM 2.0 commands

Message ID 20170303151912.14752-3-jarkko.sakkinen@linux.intel.com (mailing list archive)
State New, archived
Headers show

Commit Message

Jarkko Sakkinen March 3, 2017, 3:19 p.m. UTC
Check for every TPM 2.0 command that the command code is supported and
the command buffer has at least the length that can contain the header
and the handle area.

For ContextSave and FlushContext we mark the body to be part of the
handle area. This gives validation for these commands at zero
cost, including the body of the command.

The more important reason for this is that we can virtualize these
commands in the same way as you would virtualize the handle area of a
command.

Signed-off-by: Jarkko Sakkinen <jarkko.sakkinen@linux.intel.com>
Tested-by: James Bottomley <James.Bottomley@HansenPartnership.com>
Reviewed-by: James Bottomley <James.Bottomley@HansenPartnership.com>
---
 drivers/char/tpm/tpm-interface.c | 38 +++++++++++++++++-
 drivers/char/tpm/tpm.h           | 15 +++++++
 drivers/char/tpm/tpm2-cmd.c      | 84 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 132 insertions(+), 5 deletions(-)

Comments

Alexander Steffen March 17, 2017, 3:40 p.m. UTC | #1
> Check for every TPM 2.0 command that the command code is supported and
> the command buffer has at least the length that can contain the header
> and the handle area.

This breaks several use cases for me:

1. I've got a TPM that implements vendor-specific command codes. Those cannot be send to the TPM anymore, but are rejected with EINVAL.

2. When upgrading the firmware on my TPM, it switches to a non-standard communication mode for the upgrade process and does not communicate using TPM2.0 commands during this time. Rejecting non-TPM2.0 commands means upgrading won't be possible anymore.

3. I'd like to use the kernel driver to test my TPM implementation. So for example, I send an invalid command code to the TPM and expect TPM_RC_COMMAND_CODE in response, but now I get EINVAL instead and the TPM never sees the command.

>From my point of view, the kernel driver should provide a transparent communication channel to the TPM. Whatever I write to /dev/tpm<n> should arrive at the TPM device, so that the TPM can handle it and return the appropriate response. Otherwise, you'll end up reimplementing all the command handling logic, that is already part of the TPM's job, and as soon as you miss one case and behave differently than the TPM, something relying on this behavior will break.

I see two possible solutions:

1. When the driver does not know a command code, it passes through the command unmodified. This bears the risk of unknown side effects though, so TPM spaces might not be as independent as they should be.

2. Since the command code lookup is only really necessary for TPM spaces, it only gets activated when space != NULL. So the change will not affect /dev/tpm<n>, but only the new /dev/tpmrm<n>. As /dev/tpmrm<n> is not meant to be a transparent interface anyway, rejecting unknown commands is acceptable.

Alexander
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Jason Gunthorpe March 17, 2017, 4:16 p.m. UTC | #2
On Fri, Mar 17, 2017 at 03:40:15PM +0000, Alexander.Steffen@infineon.com wrote:

> 1. I've got a TPM that implements vendor-specific command
> codes. Those cannot be send to the TPM anymore, but are rejected
> with EINVAL.
> 
> 2. When upgrading the firmware on my TPM, it switches to a
> non-standard communication mode for the upgrade process and does not
> communicate using TPM2.0 commands during this time. Rejecting
> non-TPM2.0 commands means upgrading won't be possible anymore.

How non standard? Is the basic header even there? Are the lengths
and status code right?

This might be an argument to add a 'raw' ioctl or something
specifically for this special case.

Jason

------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Peter Huewe March 17, 2017, 4:35 p.m. UTC | #3
> 1. I've got a TPM that implements vendor-specific command codes. Those
> cannot be send to the TPM anymore, but are rejected with EINVAL.
>
>> 2. When upgrading the firmware on my TPM, it switches to a
>> non-standard communication mode for the upgrade process and does not
>> communicate using TPM2.0 commands during this time. Rejecting
>> non-TPM2.0 commands means upgrading won't be possible anymore.

>How non standard? Is the basic header even there? Are the lengths and status code right?

>This might be an argument to add a 'raw' ioctl or something specifically for this special case.

It follows the regular TPM command syntax and looks something like 1.2 commands.

Peter



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Jarkko Sakkinen March 17, 2017, 8:42 p.m. UTC | #4
On Fri, Mar 17, 2017 at 03:40:15PM +0000, Alexander.Steffen@infineon.com wrote:
> > Check for every TPM 2.0 command that the command code is supported and
> > the command buffer has at least the length that can contain the header
> > and the handle area.
> 
> This breaks several use cases for me:

Thank you for reporting these. This is really great feedback to get.

> 1. I've got a TPM that implements vendor-specific command codes. Those
> cannot be send to the TPM anymore, but are rejected with EINVAL.
> 
> 2. When upgrading the firmware on my TPM, it switches to a
> non-standard communication mode for the upgrade process and does not
> communicate using TPM2.0 commands during this time. Rejecting
> non-TPM2.0 commands means upgrading won't be possible anymore.
> 
> 3. I'd like to use the kernel driver to test my TPM implementation. So
> for example, I send an invalid command code to the TPM and expect
> TPM_RC_COMMAND_CODE in response, but now I get EINVAL instead and the
> TPM never sees the command.
> 
> From my point of view, the kernel driver should provide a transparent
> communication channel to the TPM. Whatever I write to /dev/tpm<n>
> should arrive at the TPM device, so that the TPM can handle it and
> return the appropriate response. Otherwise, you'll end up
> reimplementing all the command handling logic, that is already part of
> the TPM's job, and as soon as you miss one case and behave differently
> than the TPM, something relying on this behavior will break.
> 
> I see two possible solutions:
> 
> 1. When the driver does not know a command code, it passes through the
> command unmodified. This bears the risk of unknown side effects
> though, so TPM spaces might not be as independent as they should be.
> 
> 2. Since the command code lookup is only really necessary for TPM
> spaces, it only gets activated when space != NULL. So the change will
> not affect /dev/tpm<n>, but only the new /dev/tpmrm<n>. As
> /dev/tpmrm<n> is not meant to be a transparent interface anyway,
> rejecting unknown commands is acceptable.
> 
> Alexander

I think the most straight-forward way to sort this out would be to limit
validation to the resource manager. If I send a fix, would you care to
test it? If your issues get sorted, I'll squash it to the existing
commits.

Thanks again!

/Jarkko

------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Alexander Steffen March 20, 2017, 9:54 a.m. UTC | #5
>>> 2. When upgrading the firmware on my TPM, it switches to a
>>> non-standard communication mode for the upgrade process and does not
>>> communicate using TPM2.0 commands during this time. Rejecting
>>> non-TPM2.0 commands means upgrading won't be possible anymore.
>>
>> How non standard? Is the basic header even there? Are the lengths and
>> status code right?
>>
>> This might be an argument to add a 'raw' ioctl or something specifically
>> for this special case.
>
> It follows the regular TPM command syntax and looks something like 1.2
> commands.

Yep, so most of it already works with the current implementation.

There are a few special cases that need some thought though. For example, it is possible to use an upgrade to switch the TPM family from 1.2 to 2.0 (or vice versa). In this case it seems useful to let the kernel reinitialize the TPM driver, so it uses the correct timeouts for communication, activates the correct features (resource manager or not?), etc., without needing to reboot the system.

Another problem arises when the upgrade process is interrupted, e.g. because power is lost. Then the TPM is stuck in its non-standard upgrade mode, so the kernel does not recognize it as a valid TPM device and does not export /dev/tpm<n>. But without the device node the upgrader is unable to restart the upgrade process, leaving the TPM forever inaccessible.

I'll try to work on those problems in the coming weeks and provide the fixes. Any input is appreciated.

Alexander
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Alexander Steffen March 20, 2017, 9:56 a.m. UTC | #6
> I think the most straight-forward way to sort this out would be to limit
> validation to the resource manager.

Sounds good to me.

> If I send a fix, would you care to test it?

Sure, will do.

Alexander
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Jason Gunthorpe March 20, 2017, 5:23 p.m. UTC | #7
On Mon, Mar 20, 2017 at 09:54:41AM +0000, Alexander.Steffen@infineon.com wrote:
> >>> 2. When upgrading the firmware on my TPM, it switches to a
> >>> non-standard communication mode for the upgrade process and does not
> >>> communicate using TPM2.0 commands during this time. Rejecting
> >>> non-TPM2.0 commands means upgrading won't be possible anymore.
> >>
> >> How non standard? Is the basic header even there? Are the lengths and
> >> status code right?
> >>
> >> This might be an argument to add a 'raw' ioctl or something specifically
> >> for this special case.
> >
> > It follows the regular TPM command syntax and looks something like 1.2
> > commands.
> 
> Yep, so most of it already works with the current implementation.
> 
> There are a few special cases that need some thought though. For
> example, it is possible to use an upgrade to switch the TPM family
> from 1.2 to 2.0 (or vice versa). In this case it seems useful to let
> the kernel reinitialize the TPM driver, so it uses the correct
> timeouts for communication, activates the correct features (resource
> manager or not?), etc., without needing to reboot the system.

It would be nice to do this via plug/unplug with existing sysfs
machinery.

> Another problem arises when the upgrade process is interrupted,
> e.g. because power is lost. Then the TPM is stuck in its
> non-standard upgrade mode, so the kernel does not recognize it as a
> valid TPM device and does not export /dev/tpm<n>. But without the
> device node the upgrader is unable to restart the upgrade process,
> leaving the TPM forever inaccessible.

I guess you'd have to teach the TPM core about a new chip mode besides
1.2, 2.0 - some kind of 'upgrade' mode.

So the flow would be to send the upgrade command, unplug/replug the
driver to switch to 'upgrade' mode (which could happen if there was a
reboot?) do the upgrade, then unplug/replug to rediscover the 'new'
TPM.

Jason

------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Ken Goldman March 20, 2017, 7:42 p.m. UTC | #8
On 3/20/2017 5:54 AM, Alexander.Steffen@infineon.com wrote:
>
> There are a few special cases that need some thought though. For
> example, it is possible to use an upgrade to switch the TPM family
> from 1.2 to 2.0 (or vice versa). In this case it seems useful to let
> the kernel reinitialize the TPM driver, so it uses the correct
> timeouts for communication, activates the correct features (resource
> manager or not?), etc., without needing to reboot the system.

In practice, would a TPM upgrade from TPM 1.2 to TPM 2.0 even occur 
without a reboot?  Is it an important use case?

1 - It would leave the SHA-256 PCRs in the reset state.

2 - It's possible that this upgrade would also require a BIOS upgrade.


------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Alexander Steffen March 21, 2017, 3:44 p.m. UTC | #9
> > There are a few special cases that need some thought though. For
> > example, it is possible to use an upgrade to switch the TPM family
> > from 1.2 to 2.0 (or vice versa). In this case it seems useful to let
> > the kernel reinitialize the TPM driver, so it uses the correct
> > timeouts for communication, activates the correct features (resource
> > manager or not?), etc., without needing to reboot the system.
> 
> In practice, would a TPM upgrade from TPM 1.2 to TPM 2.0 even occur
> without a reboot?  Is it an important use case?
> 
> 1 - It would leave the SHA-256 PCRs in the reset state.
> 
> 2 - It's possible that this upgrade would also require a BIOS upgrade.

For a traditional PC and when your goal is platform integrity, a reboot is probably the way to go. But in an embedded environment where there is no BIOS or if you use the TPM more like a smartcard just to store some keys (or generate random numbers), a reboot is unnecessary and it is more comfortable to avoid it.

We probably should inform the kernel before the upgrade anyway, so that it can shut down the TPM gracefully (and maybe switch to the upgrade mode, as Jason suggested). With that infrastructure in place, it does not seem like a lot of effort to also let it switch the TPM back to normal operation mode once the upgrade is complete.

Alexander
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
Jarkko Sakkinen March 27, 2017, 5:25 a.m. UTC | #10
On Mon, Mar 20, 2017 at 09:56:17AM +0000, Alexander.Steffen@infineon.com wrote:
> > I think the most straight-forward way to sort this out would be to limit
> > validation to the resource manager.
> 
> Sounds good to me.
> 
> > If I send a fix, would you care to test it?
> 
> Sure, will do.
> 
> Alexander

I sent the patch. Please check it out.

/Jarkko

------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
diff mbox

Patch

diff --git a/drivers/char/tpm/tpm-interface.c b/drivers/char/tpm/tpm-interface.c
index 708d356..20b1fe3 100644
--- a/drivers/char/tpm/tpm-interface.c
+++ b/drivers/char/tpm/tpm-interface.c
@@ -328,6 +328,42 @@  unsigned long tpm_calc_ordinal_duration(struct tpm_chip *chip,
 }
 EXPORT_SYMBOL_GPL(tpm_calc_ordinal_duration);
 
+static bool tpm_validate_command(struct tpm_chip *chip, const u8 *cmd,
+				 size_t len)
+{
+	const struct tpm_input_header *header = (const void *)cmd;
+	int i;
+	u32 cc;
+	u32 attrs;
+	unsigned int nr_handles;
+
+	if (len < TPM_HEADER_SIZE)
+		return false;
+
+	if (chip->flags & TPM_CHIP_FLAG_TPM2 && chip->nr_commands) {
+		cc = be32_to_cpu(header->ordinal);
+
+		i = tpm2_find_cc(chip, cc);
+		if (i < 0) {
+			dev_dbg(&chip->dev, "0x%04X is an invalid command\n",
+				cc);
+			return false;
+		}
+
+		attrs = chip->cc_attrs_tbl[i];
+		nr_handles =
+			4 * ((attrs >> TPM2_CC_ATTR_CHANDLES) & GENMASK(2, 0));
+		if (len < TPM_HEADER_SIZE + 4 * nr_handles)
+			goto err_len;
+	}
+
+	return true;
+err_len:
+	dev_dbg(&chip->dev,
+		"%s: insufficient command length %zu", __func__, len);
+	return false;
+}
+
 /**
  * tmp_transmit - Internal kernel interface to transmit TPM commands.
  *
@@ -348,7 +384,7 @@  ssize_t tpm_transmit(struct tpm_chip *chip, const u8 *buf, size_t bufsiz,
 	u32 count, ordinal;
 	unsigned long stop;
 
-	if (bufsiz < TPM_HEADER_SIZE)
+	if (!tpm_validate_command(chip, buf, bufsiz))
 		return -EINVAL;
 
 	if (bufsiz > TPM_BUFSIZE)
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index 4937b56..8568dda 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -114,6 +114,7 @@  enum tpm2_command_codes {
 	TPM2_CC_CREATE		= 0x0153,
 	TPM2_CC_LOAD		= 0x0157,
 	TPM2_CC_UNSEAL		= 0x015E,
+	TPM2_CC_CONTEXT_SAVE	= 0x0162,
 	TPM2_CC_FLUSH_CONTEXT	= 0x0165,
 	TPM2_CC_GET_CAPABILITY	= 0x017A,
 	TPM2_CC_GET_RANDOM	= 0x017B,
@@ -127,15 +128,25 @@  enum tpm2_permanent_handles {
 };
 
 enum tpm2_capabilities {
+	TPM2_CAP_COMMANDS	= 2,
 	TPM2_CAP_PCRS		= 5,
 	TPM2_CAP_TPM_PROPERTIES = 6,
 };
 
+enum tpm2_properties {
+	TPM_PT_TOTAL_COMMANDS	= 0x0129,
+};
+
 enum tpm2_startup_types {
 	TPM2_SU_CLEAR	= 0x0000,
 	TPM2_SU_STATE	= 0x0001,
 };
 
+enum tpm2_cc_attrs {
+	TPM2_CC_ATTR_CHANDLES	= 25,
+	TPM2_CC_ATTR_RHANDLE	= 28,
+};
+
 #define TPM_VID_INTEL    0x8086
 #define TPM_VID_WINBOND  0x1050
 #define TPM_VID_STM      0x104A
@@ -199,6 +210,9 @@  struct tpm_chip {
 	acpi_handle acpi_dev_handle;
 	char ppi_version[TPM_PPI_VERSION_LEN + 1];
 #endif /* CONFIG_ACPI */
+
+	u32 nr_commands;
+	u32 *cc_attrs_tbl;
 };
 
 #define to_tpm_chip(d) container_of(d, struct tpm_chip, dev)
@@ -554,4 +568,5 @@  int tpm2_auto_startup(struct tpm_chip *chip);
 void tpm2_shutdown(struct tpm_chip *chip, u16 shutdown_type);
 unsigned long tpm2_calc_ordinal_duration(struct tpm_chip *chip, u32 ordinal);
 int tpm2_probe(struct tpm_chip *chip);
+int tpm2_find_cc(struct tpm_chip *chip, u32 cc);
 #endif
diff --git a/drivers/char/tpm/tpm2-cmd.c b/drivers/char/tpm/tpm2-cmd.c
index 881aea9..5efe9c4 100644
--- a/drivers/char/tpm/tpm2-cmd.c
+++ b/drivers/char/tpm/tpm2-cmd.c
@@ -1067,15 +1067,76 @@  static ssize_t tpm2_get_pcr_allocation(struct tpm_chip *chip)
 	return rc;
 }
 
+static int tpm2_get_cc_attrs_tbl(struct tpm_chip *chip)
+{
+	struct tpm_buf buf;
+	u32 nr_commands;
+	u32 *attrs;
+	u32 cc;
+	int i;
+	int rc;
+
+	rc = tpm2_get_tpm_pt(chip, TPM_PT_TOTAL_COMMANDS, &nr_commands, NULL);
+	if (rc)
+		goto out;
+
+	if (nr_commands > 0xFFFFF) {
+		rc = -EFAULT;
+		goto out;
+	}
+
+	chip->cc_attrs_tbl = devm_kzalloc(&chip->dev, 4 * nr_commands,
+					  GFP_KERNEL);
+
+	rc = tpm_buf_init(&buf, TPM2_ST_NO_SESSIONS, TPM2_CC_GET_CAPABILITY);
+	if (rc)
+		goto out;
+
+	tpm_buf_append_u32(&buf, TPM2_CAP_COMMANDS);
+	tpm_buf_append_u32(&buf, TPM2_CC_FIRST);
+	tpm_buf_append_u32(&buf, nr_commands);
+
+	rc = tpm_transmit_cmd(chip, buf.data, PAGE_SIZE, 9 + 4 * nr_commands,
+			      0, NULL);
+	if (rc) {
+		tpm_buf_destroy(&buf);
+		goto out;
+	}
+
+	if (nr_commands !=
+	    be32_to_cpup((__be32 *)&buf.data[TPM_HEADER_SIZE + 5])) {
+		tpm_buf_destroy(&buf);
+		goto out;
+	}
+
+	chip->nr_commands = nr_commands;
+
+	attrs = (u32 *)&buf.data[TPM_HEADER_SIZE + 9];
+	for (i = 0; i < nr_commands; i++, attrs++) {
+		chip->cc_attrs_tbl[i] = be32_to_cpup(attrs);
+		cc = chip->cc_attrs_tbl[i] & 0xFFFF;
+
+		if (cc == TPM2_CC_CONTEXT_SAVE || cc == TPM2_CC_FLUSH_CONTEXT) {
+			chip->cc_attrs_tbl[i] &=
+				~(GENMASK(2, 0) << TPM2_CC_ATTR_CHANDLES);
+			chip->cc_attrs_tbl[i] |= 1 << TPM2_CC_ATTR_CHANDLES;
+		}
+	}
+
+	tpm_buf_destroy(&buf);
+
+out:
+	if (rc > 0)
+		rc = -ENODEV;
+	return rc;
+}
+
 /**
  * tpm2_auto_startup - Perform the standard automatic TPM initialization
  *                     sequence
  * @chip: TPM chip to use
  *
- * Initializes timeout values for operation and command durations, conducts
- * a self-test and reads the list of active PCR banks.
- *
- * Return: 0 on success. Otherwise, a system error code is returned.
+ * Returns 0 on success, < 0 in case of fatal error.
  */
 int tpm2_auto_startup(struct tpm_chip *chip)
 {
@@ -1104,9 +1165,24 @@  int tpm2_auto_startup(struct tpm_chip *chip)
 	}
 
 	rc = tpm2_get_pcr_allocation(chip);
+	if (rc)
+		goto out;
+
+	rc = tpm2_get_cc_attrs_tbl(chip);
 
 out:
 	if (rc > 0)
 		rc = -ENODEV;
 	return rc;
 }
+
+int tpm2_find_cc(struct tpm_chip *chip, u32 cc)
+{
+	int i;
+
+	for (i = 0; i < chip->nr_commands; i++)
+		if (cc == (chip->cc_attrs_tbl[i] & GENMASK(15, 0)))
+			return i;
+
+	return -1;
+}