* [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:41 ` Matthew Garrett
` (2 more replies)
2026-10-08 13:20 ` [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features Matthew Garrett
` (16 subsequent siblings)
17 siblings, 3 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
TPM NV indexes can be used for various purposes, including using them as
PCRs without depleting the limited number of hardware PCRs. It would be
beneficial to have one that's under control of the kernel in order to be
able to prove whether certain TPM operations occurred within the kernel
or not.
Reserve NV index 0x014c4853 for use by the kernel, and filter commands
submitted through /dev/tpm* and /dev/tpmrm* so that userspace cannot
undefine or modify it. Blocking definition is more awkward so let's
allow that for now, not being able to modify it means there's nothing
interesting they can do there. The filter simply validates which handle
the relevant set of commands is referring to and returns -EPERM if it's
the kernel one.
NV indexes are from allocated ranges and this is a range allocated to
Linux, so there should be no userland depending on the ability to access
this - but let's gate it behind a default N config option anyway.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/Kconfig | 8 ++++
drivers/char/tpm/tpm-dev-common.c | 67 +++++++++++++++++++++++++++++++
include/linux/tpm.h | 6 +++
include/linux/tpm_command.h | 10 +++++
4 files changed, 91 insertions(+)
diff --git a/drivers/char/tpm/Kconfig b/drivers/char/tpm/Kconfig
index 5f672f2c01b0..a454b63edca5 100644
--- a/drivers/char/tpm/Kconfig
+++ b/drivers/char/tpm/Kconfig
@@ -253,5 +253,13 @@ config TCG_SVSM
level (usually VMPL0). To compile this driver as a module, choose M
here; the module will be called tpm_svsm.
+config TCG_TPM_KERNEL_NVINDEX
+ bool "Kernel-only NV index"
+ help
+ Allocate a TPM NV index and block userland from modifying it. This
+ allows the kernel to develop a unique state that distinguishes it
+ from userspace, making it possible to prove that a TPM object
+ was created by the kernel.
+
source "drivers/char/tpm/st33zp24/Kconfig"
endif # TCG_TPM
diff --git a/drivers/char/tpm/tpm-dev-common.c b/drivers/char/tpm/tpm-dev-common.c
index f942c0c8e402..1fd93cdaf306 100644
--- a/drivers/char/tpm/tpm-dev-common.c
+++ b/drivers/char/tpm/tpm-dev-common.c
@@ -15,18 +15,85 @@
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
+#include <linux/unaligned.h>
#include <linux/workqueue.h>
#include "tpm.h"
#include "tpm-dev.h"
static struct workqueue_struct *tpm_dev_wq;
+#ifdef CONFIG_TCG_TPM_KERNEL_NVINDEX
+/*
+ * Commands that undefine or modify an NV index, and the position of the
+ * NV index in the command's handle area.
+ */
+static const struct {
+ u32 cc;
+ unsigned int handle;
+} tpm2_nv_modify_cmds[] = {
+ { TPM2_CC_NV_UNDEFINE_SPACE_SPECIAL, 0 },
+ { TPM2_CC_NV_UNDEFINE_SPACE, 1 },
+ { TPM2_CC_NV_INCREMENT, 1 },
+ { TPM2_CC_NV_SET_BITS, 1 },
+ { TPM2_CC_NV_EXTEND, 1 },
+ { TPM2_CC_NV_WRITE, 1 },
+ { TPM2_CC_NV_WRITE_LOCK, 1 },
+ { TPM2_CC_NV_CHANGE_AUTH, 0 },
+ { TPM2_CC_NV_READ_LOCK, 1 },
+};
+
+/*
+ * Returns true if a command from userspace attempts to define, undefine
+ * or modify the kernel-owned NV index.
+ */
+static bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
+{
+ const struct tpm_header *header = (const void *)buf;
+ size_t len = min_t(size_t, be32_to_cpu(header->length), bufsiz);
+ u32 cc;
+ int i, index;
+
+ if (len < TPM_HEADER_SIZE)
+ return false;
+
+ cc = be32_to_cpu(header->ordinal);
+
+ for (i = 0; i < ARRAY_SIZE(tpm2_nv_modify_cmds); i++) {
+ size_t offset;
+
+ if (tpm2_nv_modify_cmds[i].cc != cc)
+ continue;
+
+ offset = TPM_HEADER_SIZE +
+ tpm2_nv_modify_cmds[i].handle * sizeof(u32);
+ if (len < offset + sizeof(u32))
+ return false;
+
+ index = get_unaligned_be32(&buf[offset]);
+
+ if (index == TPM2_KERNEL_NV_INDEX)
+ return true;
+ }
+
+ return false;
+}
+#else
+static inline bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
+{
+ return false;
+}
+#endif
+
static ssize_t tpm_dev_transmit(struct tpm_chip *chip, struct tpm_space *space,
u8 *buf, size_t bufsiz)
{
struct tpm_header *header = (void *)buf;
ssize_t ret, len;
+ if ((chip->flags & TPM_CHIP_FLAG_TPM2) &&
+ tpm2_dev_cmd_is_blocked(buf, bufsiz))
+ return -EPERM;
+
if (chip->flags & TPM_CHIP_FLAG_TPM2)
tpm2_end_auth_session(chip);
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 0db277af45c3..b6b862c3be3b 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -178,6 +178,12 @@ static inline enum tpm2_mso_type tpm2_handle_mso(u32 handle)
return handle >> 24;
}
+/*
+ * NV index reserved for use by the kernel. Userspace is not permitted to
+ * undefine or modify it.
+ */
+#define TPM2_KERNEL_NV_INDEX 0x014c4853
+
#define TPM_VID_INTEL 0x8086
#define TPM_VID_WINBOND 0x1050
#define TPM_VID_STM 0x104A
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index fc446a1282e2..79f547ca6dbf 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -214,14 +214,24 @@ enum tpm2_return_codes {
enum tpm2_command_codes {
TPM2_CC_FIRST = 0x011F,
+ TPM2_CC_NV_UNDEFINE_SPACE_SPECIAL = 0x011F,
TPM2_CC_HIERARCHY_CONTROL = 0x0121,
+ TPM2_CC_NV_UNDEFINE_SPACE = 0x0122,
TPM2_CC_HIERARCHY_CHANGE_AUTH = 0x0129,
+ TPM2_CC_NV_DEFINE_SPACE = 0x012A,
TPM2_CC_CREATE_PRIMARY = 0x0131,
+ TPM2_CC_NV_INCREMENT = 0x0134,
+ TPM2_CC_NV_SET_BITS = 0x0135,
+ TPM2_CC_NV_EXTEND = 0x0136,
+ TPM2_CC_NV_WRITE = 0x0137,
+ TPM2_CC_NV_WRITE_LOCK = 0x0138,
+ TPM2_CC_NV_CHANGE_AUTH = 0x013B,
TPM2_CC_SEQUENCE_COMPLETE = 0x013E,
TPM2_CC_SELF_TEST = 0x0143,
TPM2_CC_STARTUP = 0x0144,
TPM2_CC_SHUTDOWN = 0x0145,
TPM2_CC_NV_READ = 0x014E,
+ TPM2_CC_NV_READ_LOCK = 0x014F,
TPM2_CC_CREATE = 0x0153,
TPM2_CC_LOAD = 0x0157,
TPM2_CC_SEQUENCE_UPDATE = 0x015C,
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
@ 2026-10-08 13:41 ` Matthew Garrett
2026-10-08 16:24 ` Jarkko Sakkinen
2026-10-08 16:23 ` Jarkko Sakkinen
2026-10-08 17:06 ` Ilias Apalodimas
2 siblings, 1 reply; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:41 UTC (permalink / raw)
To: Matthew Garrett
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi
On Thu, Oct 08, 2026 at 06:20:17AM -0700, Matthew Garrett wrote:
> Reserve NV index 0x014c4853 for use by the kernel, and filter commands
I should emphasise that this value is very much a placeholder - please
don't deploy this patchset anywhere until we've decided whether this is
something that should live inside the Linux UAPI group range or whether
the kernel should own an independent range.
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 13:41 ` Matthew Garrett
@ 2026-10-08 16:24 ` Jarkko Sakkinen
0 siblings, 0 replies; 31+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 16:24 UTC (permalink / raw)
To: Matthew Garrett
Cc: Matthew Garrett, keyrings, James.Bottomley, linux-integrity,
rafael, linux-pm, linux-efi
On Thu, Oct 08, 2026 at 06:41:23AM -0700, Matthew Garrett wrote:
> On Thu, Oct 08, 2026 at 06:20:17AM -0700, Matthew Garrett wrote:
>
> > Reserve NV index 0x014c4853 for use by the kernel, and filter commands
>
> I should emphasise that this value is very much a placeholder - please
> don't deploy this patchset anywhere until we've decided whether this is
> something that should live inside the Linux UAPI group range or whether
> the kernel should own an independent range.
Ya, I grabbed this from the cover letter. We keep this in mind.
Br, Jarkko
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
2026-10-08 13:41 ` Matthew Garrett
@ 2026-10-08 16:23 ` Jarkko Sakkinen
2026-10-09 8:33 ` Matthew Garrett
2026-10-08 17:06 ` Ilias Apalodimas
2 siblings, 1 reply; 31+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 16:23 UTC (permalink / raw)
To: Matthew Garrett
Cc: mjg59, keyrings, James.Bottomley, linux-integrity, rafael,
linux-pm, linux-efi
On Thu, Oct 08, 2026 at 06:20:17AM -0700, Matthew Garrett wrote:
> TPM NV indexes can be used for various purposes, including using them as
> PCRs without depleting the limited number of hardware PCRs. It would be
> beneficial to have one that's under control of the kernel in order to be
> able to prove whether certain TPM operations occurred within the kernel
> or not.
>
> Reserve NV index 0x014c4853 for use by the kernel, and filter commands
> submitted through /dev/tpm* and /dev/tpmrm* so that userspace cannot
> undefine or modify it. Blocking definition is more awkward so let's
> allow that for now, not being able to modify it means there's nothing
> interesting they can do there. The filter simply validates which handle
> the relevant set of commands is referring to and returns -EPERM if it's
> the kernel one.
>
> NV indexes are from allocated ranges and this is a range allocated to
> Linux, so there should be no userland depending on the ability to access
> this - but let's gate it behind a default N config option anyway.
>
> Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
> ---
> drivers/char/tpm/Kconfig | 8 ++++
> drivers/char/tpm/tpm-dev-common.c | 67 +++++++++++++++++++++++++++++++
> include/linux/tpm.h | 6 +++
> include/linux/tpm_command.h | 10 +++++
> 4 files changed, 91 insertions(+)
>
> diff --git a/drivers/char/tpm/Kconfig b/drivers/char/tpm/Kconfig
> index 5f672f2c01b0..a454b63edca5 100644
> --- a/drivers/char/tpm/Kconfig
> +++ b/drivers/char/tpm/Kconfig
> @@ -253,5 +253,13 @@ config TCG_SVSM
> level (usually VMPL0). To compile this driver as a module, choose M
> here; the module will be called tpm_svsm.
>
> +config TCG_TPM_KERNEL_NVINDEX
> + bool "Kernel-only NV index"
> + help
> + Allocate a TPM NV index and block userland from modifying it. This
> + allows the kernel to develop a unique state that distinguishes it
> + from userspace, making it possible to prove that a TPM object
> + was created by the kernel.
> +
> source "drivers/char/tpm/st33zp24/Kconfig"
> endif # TCG_TPM
> diff --git a/drivers/char/tpm/tpm-dev-common.c b/drivers/char/tpm/tpm-dev-common.c
> index f942c0c8e402..1fd93cdaf306 100644
> --- a/drivers/char/tpm/tpm-dev-common.c
> +++ b/drivers/char/tpm/tpm-dev-common.c
> @@ -15,18 +15,85 @@
> #include <linux/poll.h>
> #include <linux/slab.h>
> #include <linux/uaccess.h>
> +#include <linux/unaligned.h>
> #include <linux/workqueue.h>
> #include "tpm.h"
> #include "tpm-dev.h"
>
> static struct workqueue_struct *tpm_dev_wq;
>
> +#ifdef CONFIG_TCG_TPM_KERNEL_NVINDEX
> +/*
> + * Commands that undefine or modify an NV index, and the position of the
> + * NV index in the command's handle area.
> + */
> +static const struct {
> + u32 cc;
> + unsigned int handle;
> +} tpm2_nv_modify_cmds[] = {
> + { TPM2_CC_NV_UNDEFINE_SPACE_SPECIAL, 0 },
> + { TPM2_CC_NV_UNDEFINE_SPACE, 1 },
> + { TPM2_CC_NV_INCREMENT, 1 },
> + { TPM2_CC_NV_SET_BITS, 1 },
> + { TPM2_CC_NV_EXTEND, 1 },
> + { TPM2_CC_NV_WRITE, 1 },
> + { TPM2_CC_NV_WRITE_LOCK, 1 },
> + { TPM2_CC_NV_CHANGE_AUTH, 0 },
> + { TPM2_CC_NV_READ_LOCK, 1 },
> +};
> +
> +/*
> + * Returns true if a command from userspace attempts to define, undefine
> + * or modify the kernel-owned NV index.
> + */
> +static bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
This function could have less generic name.
tpm2_dev_is_kernel_nv_change?
I'm not a huge fan of this name either but it does say quite clearly
what the function does at least. Just a suggestion.
> +{
> + const struct tpm_header *header = (const void *)buf;
> + size_t len = min_t(size_t, be32_to_cpu(header->length), bufsiz);
> + u32 cc;
> + int i, index;
> +
> + if (len < TPM_HEADER_SIZE)
> + return false;
> +
> + cc = be32_to_cpu(header->ordinal);
> +
> + for (i = 0; i < ARRAY_SIZE(tpm2_nv_modify_cmds); i++) {
> + size_t offset;
> +
> + if (tpm2_nv_modify_cmds[i].cc != cc)
> + continue;
> +
> + offset = TPM_HEADER_SIZE +
> + tpm2_nv_modify_cmds[i].handle * sizeof(u32);
> + if (len < offset + sizeof(u32))
> + return false;
This will result -E2BIG in tpm_try_transmit() if I recall correctly.
Probably that's how it should be so that errnos are given at a single
location.
So not asking for anything :-)
> +
> + index = get_unaligned_be32(&buf[offset]);
> +
> + if (index == TPM2_KERNEL_NV_INDEX)
> + return true;
> + }
> +
> + return false;
> +}
> +#else
> +static inline bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
> +{
> + return false;
> +}
> +#endif
> +
> static ssize_t tpm_dev_transmit(struct tpm_chip *chip, struct tpm_space *space,
> u8 *buf, size_t bufsiz)
> {
> struct tpm_header *header = (void *)buf;
> ssize_t ret, len;
>
> + if ((chip->flags & TPM_CHIP_FLAG_TPM2) &&
> + tpm2_dev_cmd_is_blocked(buf, bufsiz))
> + return -EPERM;
> +
> if (chip->flags & TPM_CHIP_FLAG_TPM2)
> tpm2_end_auth_session(chip);
>
> diff --git a/include/linux/tpm.h b/include/linux/tpm.h
> index 0db277af45c3..b6b862c3be3b 100644
> --- a/include/linux/tpm.h
> +++ b/include/linux/tpm.h
> @@ -178,6 +178,12 @@ static inline enum tpm2_mso_type tpm2_handle_mso(u32 handle)
> return handle >> 24;
> }
>
> +/*
> + * NV index reserved for use by the kernel. Userspace is not permitted to
> + * undefine or modify it.
> + */
> +#define TPM2_KERNEL_NV_INDEX 0x014c4853
> +
> #define TPM_VID_INTEL 0x8086
> #define TPM_VID_WINBOND 0x1050
> #define TPM_VID_STM 0x104A
> diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
> index fc446a1282e2..79f547ca6dbf 100644
> --- a/include/linux/tpm_command.h
> +++ b/include/linux/tpm_command.h
> @@ -214,14 +214,24 @@ enum tpm2_return_codes {
>
> enum tpm2_command_codes {
> TPM2_CC_FIRST = 0x011F,
> + TPM2_CC_NV_UNDEFINE_SPACE_SPECIAL = 0x011F,
> TPM2_CC_HIERARCHY_CONTROL = 0x0121,
> + TPM2_CC_NV_UNDEFINE_SPACE = 0x0122,
> TPM2_CC_HIERARCHY_CHANGE_AUTH = 0x0129,
> + TPM2_CC_NV_DEFINE_SPACE = 0x012A,
> TPM2_CC_CREATE_PRIMARY = 0x0131,
> + TPM2_CC_NV_INCREMENT = 0x0134,
> + TPM2_CC_NV_SET_BITS = 0x0135,
> + TPM2_CC_NV_EXTEND = 0x0136,
> + TPM2_CC_NV_WRITE = 0x0137,
> + TPM2_CC_NV_WRITE_LOCK = 0x0138,
> + TPM2_CC_NV_CHANGE_AUTH = 0x013B,
> TPM2_CC_SEQUENCE_COMPLETE = 0x013E,
> TPM2_CC_SELF_TEST = 0x0143,
> TPM2_CC_STARTUP = 0x0144,
> TPM2_CC_SHUTDOWN = 0x0145,
> TPM2_CC_NV_READ = 0x014E,
> + TPM2_CC_NV_READ_LOCK = 0x014F,
> TPM2_CC_CREATE = 0x0153,
> TPM2_CC_LOAD = 0x0157,
> TPM2_CC_SEQUENCE_UPDATE = 0x015C,
> --
> 2.43.0
>
>
Br, Jarkko
^ permalink raw reply [flat|nested] 31+ messages in thread* Re: [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 16:23 ` Jarkko Sakkinen
@ 2026-10-09 8:33 ` Matthew Garrett
0 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-09 8:33 UTC (permalink / raw)
To: Jarkko Sakkinen
Cc: Matthew Garrett, keyrings, James.Bottomley, linux-integrity,
rafael, linux-pm, linux-efi
On Thu, Oct 08, 2026 at 07:23:02PM +0300, Jarkko Sakkinen wrote:
> > +static bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
>
> This function could have less generic name.
>
> tpm2_dev_is_kernel_nv_change?
>
> I'm not a huge fan of this name either but it does say quite clearly
> what the function does at least. Just a suggestion.
I left it that way on the assumption that it might get extended at some
point - maybe that's not a big concern.
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
2026-10-08 13:41 ` Matthew Garrett
2026-10-08 16:23 ` Jarkko Sakkinen
@ 2026-10-08 17:06 ` Ilias Apalodimas
2 siblings, 0 replies; 31+ messages in thread
From: Ilias Apalodimas @ 2026-10-08 17:06 UTC (permalink / raw)
To: Matthew Garrett
Cc: mjg59, keyrings, James.Bottomley, linux-integrity, rafael,
linux-pm, linux-efi
Hi Matthew,
On Thu, 8 Oct 2026 at 16:25, Matthew Garrett <matthewg@nvidia.com> wrote:
>
> TPM NV indexes can be used for various purposes, including using them as
> PCRs without depleting the limited number of hardware PCRs. It would be
> beneficial to have one that's under control of the kernel in order to be
> able to prove whether certain TPM operations occurred within the kernel
> or not.
>
> Reserve NV index 0x014c4853 for use by the kernel, and filter commands
> submitted through /dev/tpm* and /dev/tpmrm* so that userspace cannot
> undefine or modify it. Blocking definition is more awkward so let's
> allow that for now, not being able to modify it means there's nothing
> interesting they can do there. The filter simply validates which handle
> the relevant set of commands is referring to and returns -EPERM if it's
> the kernel one.
>
> NV indexes are from allocated ranges and this is a range allocated to
> Linux, so there should be no userland depending on the ability to access
> this - but let's gate it behind a default N config option anyway.
I think the idea is useful overall.
>
> Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
> ---
[...]
> +static bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
> +{
> + const struct tpm_header *header = (const void *)buf;
> + size_t len = min_t(size_t, be32_to_cpu(header->length), bufsiz);
I haven't read the entire code base but don't you have to check bufsiz
and make sure it's a valid header before dereferencing header->length?
> + u32 cc;
> + int i, index;
> +
> + if (len < TPM_HEADER_SIZE)
> + return false;
> +
> + cc = be32_to_cpu(header->ordinal);
> +
> + for (i = 0; i < ARRAY_SIZE(tpm2_nv_modify_cmds); i++) {
> + size_t offset;
> +
> + if (tpm2_nv_modify_cmds[i].cc != cc)
> + continue;
> +
> + offset = TPM_HEADER_SIZE +
> + tpm2_nv_modify_cmds[i].handle * sizeof(u32);
> + if (len < offset + sizeof(u32))
> + return false;
> +
> + index = get_unaligned_be32(&buf[offset]);
nit, but get_unaligned_be32() returns a u32, might as well define the
index as such
> +
> + if (index == TPM2_KERNEL_NV_INDEX)
> + return true;
> + }
> +
> + return false;
> +}
> +#else
> +static inline bool tpm2_dev_cmd_is_blocked(const u8 *buf, size_t bufsiz)
> +{
> + return false;
> +}
[...]
Regards
/Ilias
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 16:38 ` Jarkko Sakkinen
2026-10-08 13:20 ` [PATCH 03/17] tpm: Allow tpm2_start_auth_session() to start an audit session Matthew Garrett
` (15 subsequent siblings)
17 siblings, 1 reply; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
"tpm: Define a kernel-owned TPM NV index that can't be modified by
userland" adds support for restricting a TPM NV index to kernel use,
allowing us to perform TPM operations in-kernel that can be proven to be
owned by the kernel. But previous kernels didn't implement this
restriction, and so an identical proof can be generated by booting an
old kernel and setting up this NV index in userland. We need some way to
differentiate these situations, which means we need some way to change the
TPM state in a way that userland can't mimic.
Thankfully the combination of the TCG specification and our EFI boot
stub give us a mechanism to achieve this. The EFI boot stub runs before
ExitBootServices is called, and ExitBootServices is (according to the
spec) supposed to extend PCR 5. If we perform an extension of PCR 5
before ExitBootServices is called, our extension will be followed by the
ExitBootServices extension before any userland code runs. Userland code
on an old kernel will be able to perform the same extension, but only
after ExitBootServices is called, and so will end up with a different
PCR 5 value because the order of extension events matters.
Obviously this depends on the platform actually extending PCR 5 on
ExitBootServices, which is something we can't fundamentally depend on
because firmware. So, let's be careful. After performing the extension,
read the SHA 1 and SHA 256 banks (because we can't guarantee the
firmware is using both) and put those in a config table to pass up to
the runtime kernel. It can then read these values and read PCR 5. If the
values are identical then the firmware didn't perform an extension and
userland could fake the same setup, so flag this as a firmware bug and
don't enable anything. If the firmware only extended one bank then
that's still sufficient - we will end up having to rely on that single
bank, but that's still sufficient to demonstrate that we're on a new
kernel (at least, until SHA 1 is broken more than it currently is).
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/firmware/efi/Kconfig | 15 ++
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 3 +
.../firmware/efi/libstub/efi-stub-helper.c | 6 +
drivers/firmware/efi/libstub/efistub.h | 10 +-
drivers/firmware/efi/libstub/tpm.c | 170 ++++++++++++++++++
drivers/firmware/efi/tpm-security.c | 98 ++++++++++
include/linux/efi.h | 24 +++
8 files changed, 326 insertions(+), 1 deletion(-)
create mode 100644 drivers/firmware/efi/tpm-security.c
diff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig
index 29e0729299f5..c411fd4b6c93 100644
--- a/drivers/firmware/efi/Kconfig
+++ b/drivers/firmware/efi/Kconfig
@@ -180,6 +180,21 @@ config RESET_ATTACK_MITIGATION
have been evicted, since otherwise it will trigger even on clean
reboots.
+config KERNEL_TPM_SECURITY
+ bool "Prove that the kernel supported certain security features"
+ depends on EFI_STUB && TCG_TPM_KERNEL_NVINDEX=y
+ help
+ Have the EFI stub extend a fixed value into TPM PCR 5 in order to
+ indicate that the kernel implements specific security
+ functionality. This depends on the firmware extending PCR 5 when
+ ExitBootServices is called - a failure to do this by the firmware
+ will be logged.
+
+ At present, this feature proves that the kernel implements kernel
+ NV index reservation.
+
+ If unsure, say N.
+
config EFI_RCI2_TABLE
bool "EFI Runtime Configuration Interface Table Version 2 Support"
depends on X86 || COMPILE_TEST
diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile
index 8efbcf699e4f..79f7a2c977f1 100644
--- a/drivers/firmware/efi/Makefile
+++ b/drivers/firmware/efi/Makefile
@@ -30,6 +30,7 @@ obj-$(CONFIG_EFI_RCI2_TABLE) += rci2-table.o
obj-$(CONFIG_EFI_EMBEDDED_FIRMWARE) += embedded-firmware.o
obj-$(CONFIG_LOAD_UEFI_KEYS) += mokvar-table.o
obj-$(CONFIG_OVMF_DEBUG_LOG) += ovmf-debug-log.o
+obj-$(CONFIG_KERNEL_TPM_SECURITY) += tpm-security.o
obj-$(CONFIG_SYSFB) += sysfb_efi.o
diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c
index 0327a39d31fa..f8e2ecca9102 100644
--- a/drivers/firmware/efi/efi.c
+++ b/drivers/firmware/efi/efi.c
@@ -648,6 +648,9 @@ static const efi_config_table_type_t common_tables[] __initconst = {
#endif
#ifdef CONFIG_EFI_GENERIC_STUB
{LINUX_EFI_PRIMARY_DISPLAY_TABLE_GUID, &primary_display_table },
+#endif
+#ifdef CONFIG_KERNEL_TPM_SECURITY
+ {LINUX_EFI_PCR5_LOG_GUID, &efi_pcr5_log, "PCR5" },
#endif
{},
};
diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index f27f2e1f0019..996313f59827 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -435,6 +435,12 @@ efi_status_t efi_exit_boot_services(void *handle, void *priv,
if (efi_disable_pci_dma)
efi_pci_disable_bridge_busmaster();
+ /*
+ * This installs a configuration table, so must happen before the
+ * final memory map is retrieved.
+ */
+ efi_tpm_record_pcr5();
+
status = efi_get_memory_map(&map, true);
if (status != EFI_SUCCESS)
return status;
diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
index fd91fc15ec81..e4012bce6013 100644
--- a/drivers/firmware/efi/libstub/efistub.h
+++ b/drivers/firmware/efi/libstub/efistub.h
@@ -862,6 +862,7 @@ typedef u32 efi_tcg2_event_log_format;
#define INITRD_EVENT_TAG_ID 0x8F3B22ECU
#define LOAD_OPTIONS_EVENT_TAG_ID 0x8F3B22EDU
#define EV_EVENT_TAG 0x00000006U
+#define EV_EFI_ACTION 0x80000007U
#define EFI_TCG2_EVENT_HEADER_VERSION 0x1
struct efi_tcg2_event {
@@ -898,7 +899,8 @@ union efi_tcg2_protocol {
efi_physical_addr_t,
u64,
const efi_tcg2_event_t *);
- void *submit_command;
+ efi_status_t (__efiapi *submit_command)(efi_tcg2_protocol_t *,
+ u32, u8 *, u32, u8 *);
void *get_active_pcr_banks;
void *set_active_pcr_banks;
void *get_result_of_set_active_pcr_banks;
@@ -1169,6 +1171,12 @@ efi_enable_reset_attack_mitigation(void) { }
void efi_retrieve_eventlog(void);
+#ifdef CONFIG_KERNEL_TPM_SECURITY
+void efi_tpm_record_pcr5(void);
+#else
+static inline void efi_tpm_record_pcr5(void) { }
+#endif
+
struct sysfb_display_info *alloc_primary_display(void);
struct sysfb_display_info *__alloc_primary_display(void);
void free_primary_display(struct sysfb_display_info *dpy);
diff --git a/drivers/firmware/efi/libstub/tpm.c b/drivers/firmware/efi/libstub/tpm.c
index a5c6c4f163fc..f03490a6a544 100644
--- a/drivers/firmware/efi/libstub/tpm.c
+++ b/drivers/firmware/efi/libstub/tpm.c
@@ -9,6 +9,8 @@
*/
#include <linux/efi.h>
#include <linux/tpm_eventlog.h>
+#include <crypto/sha1.h>
+#include <crypto/sha2.h>
#include <asm/efi.h>
#include "efistub.h"
@@ -194,3 +196,171 @@ void efi_retrieve_eventlog(void)
efi_retrieve_tcg2_eventlog(version, log_location, log_last_entry,
truncated, final_events_table);
}
+
+#ifdef CONFIG_KERNEL_TPM_SECURITY
+#define PCR5_INDEX 5
+
+static const char pcr5_event[] = "Linux kernel TPM NVIndex support";
+static efi_guid_t pcr5_guid = LINUX_EFI_PCR5_LOG_GUID;
+
+static const struct {
+ u16 hash_alg;
+ u16 digest_size;
+} pcr5_banks[] = {
+ { TPM_ALG_SHA1, SHA1_DIGEST_SIZE },
+ { TPM_ALG_SHA256, SHA256_DIGEST_SIZE },
+};
+
+struct tpm2_pcr_read_cmd {
+ __be16 tag;
+ __be32 size;
+ __be32 cc;
+ __be32 count;
+ __be16 hash;
+ u8 size_of_select;
+ u8 select[3];
+} __packed;
+
+/* digest is sized for the largest bank; smaller digests are shorter */
+struct tpm2_pcr_read_rsp {
+ __be16 tag;
+ __be32 size;
+ __be32 rc;
+ __be32 update_counter;
+ __be32 count;
+ __be16 hash;
+ u8 size_of_select;
+ u8 select[3];
+ __be32 digest_count;
+ __be16 digest_size;
+ u8 digest[TPM2_MAX_DIGEST_SIZE];
+} __packed;
+
+static efi_status_t efi_tpm_extend_pcr5(efi_tcg2_protocol_t *tcg2)
+{
+ struct efi_tcg2_event *evt __free(efi_pool) = NULL;
+ u32 size = sizeof(*evt) + sizeof(pcr5_event) - 1;
+ efi_status_t status;
+
+ status = efi_bs_call(allocate_pool, EFI_LOADER_DATA, size,
+ (void **)&evt);
+ if (status != EFI_SUCCESS)
+ return status;
+
+ *evt = (struct efi_tcg2_event){
+ .event_size = size,
+ .event_header.header_size = sizeof(evt->event_header),
+ .event_header.header_version = EFI_TCG2_EVENT_HEADER_VERSION,
+ .event_header.pcr_index = PCR5_INDEX,
+ .event_header.event_type = EV_EFI_ACTION,
+ };
+ memcpy(evt + 1, pcr5_event, sizeof(pcr5_event) - 1);
+
+ return efi_call_proto(tcg2, hash_log_extend_event, 0,
+ (unsigned long)pcr5_event,
+ sizeof(pcr5_event) - 1, evt);
+}
+
+static efi_status_t efi_tpm_read_pcr5(efi_tcg2_protocol_t *tcg2,
+ u16 hash_alg, u16 digest_size,
+ struct tpm2_pcr_read_rsp *rsp)
+{
+ struct tpm2_pcr_read_cmd cmd = {
+ .tag = cpu_to_be16(TPM2_ST_NO_SESSIONS),
+ .size = cpu_to_be32(sizeof(cmd)),
+ .cc = cpu_to_be32(TPM2_CC_PCR_READ),
+ .count = cpu_to_be32(1),
+ .hash = cpu_to_be16(hash_alg),
+ .size_of_select = sizeof(cmd.select),
+ .select = { BIT(PCR5_INDEX) },
+ };
+ u32 rsp_size = sizeof(*rsp) - sizeof(rsp->digest) + digest_size;
+ efi_status_t status;
+
+ status = efi_call_proto(tcg2, submit_command, sizeof(cmd), (u8 *)&cmd,
+ sizeof(*rsp), (u8 *)rsp);
+ if (status != EFI_SUCCESS)
+ return status;
+
+ /*
+ * A TPM without the requested bank active returns an empty
+ * selection and no digests, so check that we got back exactly
+ * what we asked for.
+ */
+ if (be32_to_cpu(rsp->size) != rsp_size || rsp->rc ||
+ be32_to_cpu(rsp->count) != 1 ||
+ be16_to_cpu(rsp->hash) != hash_alg ||
+ rsp->size_of_select != sizeof(rsp->select) ||
+ rsp->select[0] != BIT(PCR5_INDEX) ||
+ be32_to_cpu(rsp->digest_count) != 1 ||
+ be16_to_cpu(rsp->digest_size) != digest_size)
+ return EFI_NOT_FOUND;
+
+ return EFI_SUCCESS;
+}
+
+/*
+ * Extend a fixed value into PCR 5 and publish the resulting value of each
+ * supported PCR bank in a configuration table, so that the kernel can
+ * later verify that the firmware extended PCR 5 when ExitBootServices()
+ * was called.
+ */
+void efi_tpm_record_pcr5(void)
+{
+ efi_guid_t tcg2_guid = EFI_TCG2_PROTOCOL_GUID;
+ struct linux_efi_pcr5_log *log;
+ struct tpm2_pcr_read_rsp rsp;
+ efi_tcg2_protocol_t *tcg2 = NULL;
+ efi_status_t status;
+ int i;
+
+ status = efi_bs_call(locate_protocol, &tcg2_guid, NULL, (void **)&tcg2);
+ if (status != EFI_SUCCESS || !tcg2)
+ return;
+
+ status = efi_tpm_extend_pcr5(tcg2);
+ if (status != EFI_SUCCESS) {
+ efi_warn("Failed to extend PCR 5: 0x%lx\n", status);
+ return;
+ }
+
+ status = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,
+ struct_size(log, digests, ARRAY_SIZE(pcr5_banks)),
+ (void **)&log);
+ if (status != EFI_SUCCESS) {
+ efi_err("Unable to allocate memory for PCR 5 log\n");
+ return;
+ }
+
+ memset(log, 0, struct_size(log, digests, ARRAY_SIZE(pcr5_banks)));
+ for (i = 0; i < ARRAY_SIZE(pcr5_banks); i++) {
+ struct linux_efi_pcr5_digest *d = &log->digests[log->count];
+
+ /* inactive banks are simply not recorded */
+ status = efi_tpm_read_pcr5(tcg2, pcr5_banks[i].hash_alg,
+ pcr5_banks[i].digest_size, &rsp);
+ if (status != EFI_SUCCESS)
+ continue;
+
+ d->hash_alg = pcr5_banks[i].hash_alg;
+ d->digest_size = pcr5_banks[i].digest_size;
+ memcpy(d->digest, rsp.digest, d->digest_size);
+ log->count++;
+ }
+
+ if (!log->count) {
+ efi_warn("Failed to read PCR 5\n");
+ goto err_free;
+ }
+
+ status = efi_bs_call(install_configuration_table, &pcr5_guid, log);
+ if (status != EFI_SUCCESS) {
+ efi_err("Unable to install PCR 5 log table\n");
+ goto err_free;
+ }
+ return;
+
+err_free:
+ efi_bs_call(free_pool, log);
+}
+#endif
diff --git a/drivers/firmware/efi/tpm-security.c b/drivers/firmware/efi/tpm-security.c
new file mode 100644
index 000000000000..aee497da0a55
--- /dev/null
+++ b/drivers/firmware/efi/tpm-security.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Verify that the firmware measured the ExitBootServices() invocation
+ * into PCR 5, by comparing the current value of PCR 5 against the value
+ * recorded by the EFI stub immediately before it called
+ * ExitBootServices().
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/efi.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/tpm.h>
+
+#define PCR5_INDEX 5
+
+unsigned long __initdata efi_pcr5_log = EFI_INVALID_TABLE_ADDR;
+bool kernel_tpm_security_available __ro_after_init;
+
+static int __init efi_tpm_check_pcr5(void)
+{
+ struct linux_efi_pcr5_log *log;
+ unsigned int recorded = 0, changed = 0;
+ struct tpm_chip *chip;
+ size_t size;
+ u32 count;
+ int i, rc;
+
+ if (efi_pcr5_log == EFI_INVALID_TABLE_ADDR)
+ return 0;
+
+ log = memremap(efi_pcr5_log, sizeof(*log), MEMREMAP_WB);
+ if (!log) {
+ pr_err("Failed to map PCR 5 log\n");
+ return 0;
+ }
+ count = log->count;
+ memunmap(log);
+
+ size = struct_size(log, digests, count);
+ log = memremap(efi_pcr5_log, size, MEMREMAP_WB);
+ if (!log) {
+ pr_err("Failed to map PCR 5 log\n");
+ return 0;
+ }
+
+ chip = tpm_default_chip();
+ if (!chip) {
+ pr_warn("No TPM available to verify PCR 5\n");
+ goto out;
+ }
+
+ if (!tpm_is_tpm2(chip)) {
+ pr_warn("PCR 5 log requires a TPM 2.0\n");
+ goto out_put;
+ }
+
+ for (i = 0; i < count; i++) {
+ struct linux_efi_pcr5_digest *d = &log->digests[i];
+ struct tpm_digest digest = { .alg_id = d->hash_alg };
+
+ if (d->digest_size > sizeof(d->digest)) {
+ pr_err("Invalid PCR 5 log entry for bank 0x%04x\n",
+ d->hash_alg);
+ continue;
+ }
+
+ rc = tpm_pcr_read(chip, PCR5_INDEX, &digest);
+ if (rc) {
+ pr_err("Failed to read PCR 5 bank 0x%04x: %d\n",
+ d->hash_alg, rc);
+ continue;
+ }
+
+ recorded++;
+ if (memcmp(digest.digest, d->digest, d->digest_size))
+ changed++;
+ }
+
+ if (!recorded)
+ goto out_put;
+
+ if (!changed)
+ pr_err(FW_BUG "Firmware failed to extend PCR 5 for ExitBootServices\n");
+ else if (changed != recorded)
+ pr_err(FW_BUG "Firmware only extended one PCR bank in ExitBootServices\n");
+
+ if (changed)
+ kernel_tpm_security_available = true;
+
+out_put:
+ put_device(&chip->dev);
+out:
+ memunmap(log);
+ return 0;
+}
+late_initcall(efi_tpm_check_pcr5);
diff --git a/include/linux/efi.h b/include/linux/efi.h
index aa15ff88539b..6d51a4bffbd8 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -23,6 +23,7 @@
#include <linux/pstore.h>
#include <linux/range.h>
#include <linux/reboot.h>
+#include <linux/tpm_command.h>
#include <linux/uuid.h>
#include <asm/page.h>
@@ -422,6 +423,7 @@ void efi_native_runtime_setup(void);
#define LINUX_EFI_COCO_SECRET_AREA_GUID EFI_GUID(0xadf956ad, 0xe98c, 0x484c, 0xae, 0x11, 0xb5, 0x1c, 0x7d, 0x33, 0x64, 0x47)
#define LINUX_EFI_BOOT_MEMMAP_GUID EFI_GUID(0x800f683f, 0xd08b, 0x423a, 0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4)
#define LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID EFI_GUID(0xd5d1de3c, 0x105c, 0x44f9, 0x9e, 0xa9, 0xbc, 0xef, 0x98, 0x12, 0x00, 0x31)
+#define LINUX_EFI_PCR5_LOG_GUID EFI_GUID(0xb38f9ff0, 0xb8ef, 0xe28f, 0x80, 0x8d, 0xe2, 0x9a, 0xa7, 0xef, 0xb8, 0x8f)
#define RISCV_EFI_BOOT_PROTOCOL_GUID EFI_GUID(0xccd15fec, 0x6f73, 0x4eec, 0x83, 0x95, 0x3e, 0x69, 0xe4, 0xb9, 0x40, 0xbf)
@@ -631,6 +633,28 @@ typedef struct {
extern unsigned long __ro_after_init efi_rng_seed; /* RNG Seed table */
+/*
+ * The value of PCR 5 as read by the EFI stub immediately before calling
+ * ExitBootServices(), published via the LINUX_EFI_PCR5_LOG_GUID
+ * configuration table. There is one entry for each supported PCR bank
+ * that was active.
+ */
+struct linux_efi_pcr5_digest {
+ u16 hash_alg; /* TPM_ALG_* of the PCR bank */
+ u16 digest_size;
+ u8 digest[TPM2_MAX_DIGEST_SIZE];
+};
+
+struct linux_efi_pcr5_log {
+ u32 count;
+ struct linux_efi_pcr5_digest digests[];
+};
+
+#ifdef CONFIG_KERNEL_TPM_SECURITY
+extern unsigned long efi_pcr5_log;
+extern bool kernel_tpm_security_available;
+#endif
+
/*
* All runtime access to EFI goes through this structure:
*/
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features
2026-10-08 13:20 ` [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features Matthew Garrett
@ 2026-10-08 16:38 ` Jarkko Sakkinen
0 siblings, 0 replies; 31+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 16:38 UTC (permalink / raw)
To: Matthew Garrett, Ross Philipson
Cc: mjg59, keyrings, James.Bottomley, linux-integrity, rafael,
linux-pm, linux-efi
On Thu, Oct 08, 2026 at 06:20:18AM -0700, Matthew Garrett wrote:
> "tpm: Define a kernel-owned TPM NV index that can't be modified by
> userland" adds support for restricting a TPM NV index to kernel use,
> allowing us to perform TPM operations in-kernel that can be proven to be
> owned by the kernel. But previous kernels didn't implement this
> restriction, and so an identical proof can be generated by booting an
> old kernel and setting up this NV index in userland. We need some way to
> differentiate these situations, which means we need some way to change the
> TPM state in a way that userland can't mimic.
>
> Thankfully the combination of the TCG specification and our EFI boot
> stub give us a mechanism to achieve this. The EFI boot stub runs before
> ExitBootServices is called, and ExitBootServices is (according to the
> spec) supposed to extend PCR 5. If we perform an extension of PCR 5
> before ExitBootServices is called, our extension will be followed by the
> ExitBootServices extension before any userland code runs. Userland code
> on an old kernel will be able to perform the same extension, but only
> after ExitBootServices is called, and so will end up with a different
> PCR 5 value because the order of extension events matters.
>
> Obviously this depends on the platform actually extending PCR 5 on
> ExitBootServices, which is something we can't fundamentally depend on
> because firmware. So, let's be careful. After performing the extension,
> read the SHA 1 and SHA 256 banks (because we can't guarantee the
> firmware is using both) and put those in a config table to pass up to
> the runtime kernel. It can then read these values and read PCR 5. If the
> values are identical then the firmware didn't perform an extension and
> userland could fake the same setup, so flag this as a firmware bug and
> don't enable anything. If the firmware only extended one bank then
> that's still sufficient - we will end up having to rely on that single
> bank, but that's still sufficient to demonstrate that we're on a new
> kernel (at least, until SHA 1 is broken more than it currently is).
>
> Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
> ---
> drivers/firmware/efi/Kconfig | 15 ++
> drivers/firmware/efi/Makefile | 1 +
> drivers/firmware/efi/efi.c | 3 +
> .../firmware/efi/libstub/efi-stub-helper.c | 6 +
> drivers/firmware/efi/libstub/efistub.h | 10 +-
> drivers/firmware/efi/libstub/tpm.c | 170 ++++++++++++++++++
> drivers/firmware/efi/tpm-security.c | 98 ++++++++++
> include/linux/efi.h | 24 +++
> 8 files changed, 326 insertions(+), 1 deletion(-)
> create mode 100644 drivers/firmware/efi/tpm-security.c
>
> diff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig
> index 29e0729299f5..c411fd4b6c93 100644
> --- a/drivers/firmware/efi/Kconfig
> +++ b/drivers/firmware/efi/Kconfig
> @@ -180,6 +180,21 @@ config RESET_ATTACK_MITIGATION
> have been evicted, since otherwise it will trigger even on clean
> reboots.
>
> +config KERNEL_TPM_SECURITY
> + bool "Prove that the kernel supported certain security features"
> + depends on EFI_STUB && TCG_TPM_KERNEL_NVINDEX=y
> + help
> + Have the EFI stub extend a fixed value into TPM PCR 5 in order to
> + indicate that the kernel implements specific security
> + functionality. This depends on the firmware extending PCR 5 when
> + ExitBootServices is called - a failure to do this by the firmware
> + will be logged.
> +
> + At present, this feature proves that the kernel implements kernel
> + NV index reservation.
> +
> + If unsure, say N.
> +
> config EFI_RCI2_TABLE
> bool "EFI Runtime Configuration Interface Table Version 2 Support"
> depends on X86 || COMPILE_TEST
> diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile
> index 8efbcf699e4f..79f7a2c977f1 100644
> --- a/drivers/firmware/efi/Makefile
> +++ b/drivers/firmware/efi/Makefile
> @@ -30,6 +30,7 @@ obj-$(CONFIG_EFI_RCI2_TABLE) += rci2-table.o
> obj-$(CONFIG_EFI_EMBEDDED_FIRMWARE) += embedded-firmware.o
> obj-$(CONFIG_LOAD_UEFI_KEYS) += mokvar-table.o
> obj-$(CONFIG_OVMF_DEBUG_LOG) += ovmf-debug-log.o
> +obj-$(CONFIG_KERNEL_TPM_SECURITY) += tpm-security.o
>
> obj-$(CONFIG_SYSFB) += sysfb_efi.o
>
> diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c
> index 0327a39d31fa..f8e2ecca9102 100644
> --- a/drivers/firmware/efi/efi.c
> +++ b/drivers/firmware/efi/efi.c
> @@ -648,6 +648,9 @@ static const efi_config_table_type_t common_tables[] __initconst = {
> #endif
> #ifdef CONFIG_EFI_GENERIC_STUB
> {LINUX_EFI_PRIMARY_DISPLAY_TABLE_GUID, &primary_display_table },
> +#endif
> +#ifdef CONFIG_KERNEL_TPM_SECURITY
> + {LINUX_EFI_PCR5_LOG_GUID, &efi_pcr5_log, "PCR5" },
> #endif
> {},
> };
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> index f27f2e1f0019..996313f59827 100644
> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
> @@ -435,6 +435,12 @@ efi_status_t efi_exit_boot_services(void *handle, void *priv,
> if (efi_disable_pci_dma)
> efi_pci_disable_bridge_busmaster();
>
> + /*
> + * This installs a configuration table, so must happen before the
> + * final memory map is retrieved.
> + */
> + efi_tpm_record_pcr5();
> +
> status = efi_get_memory_map(&map, true);
> if (status != EFI_SUCCESS)
> return status;
> diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
> index fd91fc15ec81..e4012bce6013 100644
> --- a/drivers/firmware/efi/libstub/efistub.h
> +++ b/drivers/firmware/efi/libstub/efistub.h
> @@ -862,6 +862,7 @@ typedef u32 efi_tcg2_event_log_format;
> #define INITRD_EVENT_TAG_ID 0x8F3B22ECU
> #define LOAD_OPTIONS_EVENT_TAG_ID 0x8F3B22EDU
> #define EV_EVENT_TAG 0x00000006U
> +#define EV_EFI_ACTION 0x80000007U
> #define EFI_TCG2_EVENT_HEADER_VERSION 0x1
>
> struct efi_tcg2_event {
> @@ -898,7 +899,8 @@ union efi_tcg2_protocol {
> efi_physical_addr_t,
> u64,
> const efi_tcg2_event_t *);
> - void *submit_command;
> + efi_status_t (__efiapi *submit_command)(efi_tcg2_protocol_t *,
> + u32, u8 *, u32, u8 *);
> void *get_active_pcr_banks;
> void *set_active_pcr_banks;
> void *get_result_of_set_active_pcr_banks;
> @@ -1169,6 +1171,12 @@ efi_enable_reset_attack_mitigation(void) { }
>
> void efi_retrieve_eventlog(void);
>
> +#ifdef CONFIG_KERNEL_TPM_SECURITY
> +void efi_tpm_record_pcr5(void);
> +#else
> +static inline void efi_tpm_record_pcr5(void) { }
> +#endif
> +
> struct sysfb_display_info *alloc_primary_display(void);
> struct sysfb_display_info *__alloc_primary_display(void);
> void free_primary_display(struct sysfb_display_info *dpy);
> diff --git a/drivers/firmware/efi/libstub/tpm.c b/drivers/firmware/efi/libstub/tpm.c
> index a5c6c4f163fc..f03490a6a544 100644
> --- a/drivers/firmware/efi/libstub/tpm.c
> +++ b/drivers/firmware/efi/libstub/tpm.c
> @@ -9,6 +9,8 @@
> */
> #include <linux/efi.h>
> #include <linux/tpm_eventlog.h>
> +#include <crypto/sha1.h>
> +#include <crypto/sha2.h>
> #include <asm/efi.h>
>
> #include "efistub.h"
> @@ -194,3 +196,171 @@ void efi_retrieve_eventlog(void)
> efi_retrieve_tcg2_eventlog(version, log_location, log_last_entry,
> truncated, final_events_table);
> }
> +
> +#ifdef CONFIG_KERNEL_TPM_SECURITY
> +#define PCR5_INDEX 5
> +
> +static const char pcr5_event[] = "Linux kernel TPM NVIndex support";
> +static efi_guid_t pcr5_guid = LINUX_EFI_PCR5_LOG_GUID;
> +
> +static const struct {
> + u16 hash_alg;
> + u16 digest_size;
> +} pcr5_banks[] = {
> + { TPM_ALG_SHA1, SHA1_DIGEST_SIZE },
> + { TPM_ALG_SHA256, SHA256_DIGEST_SIZE },
> +};
> +
> +struct tpm2_pcr_read_cmd {
> + __be16 tag;
> + __be32 size;
> + __be32 cc;
> + __be32 count;
> + __be16 hash;
> + u8 size_of_select;
> + u8 select[3];
> +} __packed;
> +
> +/* digest is sized for the largest bank; smaller digests are shorter */
> +struct tpm2_pcr_read_rsp {
> + __be16 tag;
> + __be32 size;
> + __be32 rc;
> + __be32 update_counter;
> + __be32 count;
> + __be16 hash;
> + u8 size_of_select;
> + u8 select[3];
> + __be32 digest_count;
> + __be16 digest_size;
> + u8 digest[TPM2_MAX_DIGEST_SIZE];
> +} __packed;
> +
> +static efi_status_t efi_tpm_extend_pcr5(efi_tcg2_protocol_t *tcg2)
> +{
> + struct efi_tcg2_event *evt __free(efi_pool) = NULL;
> + u32 size = sizeof(*evt) + sizeof(pcr5_event) - 1;
> + efi_status_t status;
> +
> + status = efi_bs_call(allocate_pool, EFI_LOADER_DATA, size,
> + (void **)&evt);
> + if (status != EFI_SUCCESS)
> + return status;
> +
> + *evt = (struct efi_tcg2_event){
> + .event_size = size,
> + .event_header.header_size = sizeof(evt->event_header),
> + .event_header.header_version = EFI_TCG2_EVENT_HEADER_VERSION,
> + .event_header.pcr_index = PCR5_INDEX,
> + .event_header.event_type = EV_EFI_ACTION,
> + };
> + memcpy(evt + 1, pcr5_event, sizeof(pcr5_event) - 1);
> +
> + return efi_call_proto(tcg2, hash_log_extend_event, 0,
> + (unsigned long)pcr5_event,
> + sizeof(pcr5_event) - 1, evt);
> +}
> +
> +static efi_status_t efi_tpm_read_pcr5(efi_tcg2_protocol_t *tcg2,
> + u16 hash_alg, u16 digest_size,
> + struct tpm2_pcr_read_rsp *rsp)
> +{
> + struct tpm2_pcr_read_cmd cmd = {
> + .tag = cpu_to_be16(TPM2_ST_NO_SESSIONS),
> + .size = cpu_to_be32(sizeof(cmd)),
> + .cc = cpu_to_be32(TPM2_CC_PCR_READ),
> + .count = cpu_to_be32(1),
> + .hash = cpu_to_be16(hash_alg),
> + .size_of_select = sizeof(cmd.select),
> + .select = { BIT(PCR5_INDEX) },
> + };
> + u32 rsp_size = sizeof(*rsp) - sizeof(rsp->digest) + digest_size;
> + efi_status_t status;
> +
> + status = efi_call_proto(tcg2, submit_command, sizeof(cmd), (u8 *)&cmd,
> + sizeof(*rsp), (u8 *)rsp);
> + if (status != EFI_SUCCESS)
> + return status;
> +
> + /*
> + * A TPM without the requested bank active returns an empty
> + * selection and no digests, so check that we got back exactly
> + * what we asked for.
> + */
> + if (be32_to_cpu(rsp->size) != rsp_size || rsp->rc ||
> + be32_to_cpu(rsp->count) != 1 ||
> + be16_to_cpu(rsp->hash) != hash_alg ||
> + rsp->size_of_select != sizeof(rsp->select) ||
> + rsp->select[0] != BIT(PCR5_INDEX) ||
> + be32_to_cpu(rsp->digest_count) != 1 ||
> + be16_to_cpu(rsp->digest_size) != digest_size)
> + return EFI_NOT_FOUND;
> +
> + return EFI_SUCCESS;
> +}
> +
> +/*
> + * Extend a fixed value into PCR 5 and publish the resulting value of each
> + * supported PCR bank in a configuration table, so that the kernel can
> + * later verify that the firmware extended PCR 5 when ExitBootServices()
> + * was called.
> + */
> +void efi_tpm_record_pcr5(void)
> +{
> + efi_guid_t tcg2_guid = EFI_TCG2_PROTOCOL_GUID;
> + struct linux_efi_pcr5_log *log;
> + struct tpm2_pcr_read_rsp rsp;
> + efi_tcg2_protocol_t *tcg2 = NULL;
> + efi_status_t status;
> + int i;
> +
> + status = efi_bs_call(locate_protocol, &tcg2_guid, NULL, (void **)&tcg2);
> + if (status != EFI_SUCCESS || !tcg2)
> + return;
> +
> + status = efi_tpm_extend_pcr5(tcg2);
> + if (status != EFI_SUCCESS) {
> + efi_warn("Failed to extend PCR 5: 0x%lx\n", status);
> + return;
> + }
> +
> + status = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,
> + struct_size(log, digests, ARRAY_SIZE(pcr5_banks)),
> + (void **)&log);
> + if (status != EFI_SUCCESS) {
> + efi_err("Unable to allocate memory for PCR 5 log\n");
> + return;
> + }
> +
> + memset(log, 0, struct_size(log, digests, ARRAY_SIZE(pcr5_banks)));
> + for (i = 0; i < ARRAY_SIZE(pcr5_banks); i++) {
> + struct linux_efi_pcr5_digest *d = &log->digests[log->count];
> +
> + /* inactive banks are simply not recorded */
> + status = efi_tpm_read_pcr5(tcg2, pcr5_banks[i].hash_alg,
> + pcr5_banks[i].digest_size, &rsp);
> + if (status != EFI_SUCCESS)
> + continue;
> +
> + d->hash_alg = pcr5_banks[i].hash_alg;
> + d->digest_size = pcr5_banks[i].digest_size;
> + memcpy(d->digest, rsp.digest, d->digest_size);
> + log->count++;
> + }
> +
> + if (!log->count) {
> + efi_warn("Failed to read PCR 5\n");
> + goto err_free;
> + }
> +
> + status = efi_bs_call(install_configuration_table, &pcr5_guid, log);
> + if (status != EFI_SUCCESS) {
> + efi_err("Unable to install PCR 5 log table\n");
> + goto err_free;
> + }
> + return;
> +
> +err_free:
> + efi_bs_call(free_pool, log);
> +}
> +#endif
> diff --git a/drivers/firmware/efi/tpm-security.c b/drivers/firmware/efi/tpm-security.c
> new file mode 100644
> index 000000000000..aee497da0a55
> --- /dev/null
> +++ b/drivers/firmware/efi/tpm-security.c
> @@ -0,0 +1,98 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Verify that the firmware measured the ExitBootServices() invocation
> + * into PCR 5, by comparing the current value of PCR 5 against the value
> + * recorded by the EFI stub immediately before it called
> + * ExitBootServices().
> + */
> +
> +#define pr_fmt(fmt) "efi: " fmt
> +
> +#include <linux/efi.h>
> +#include <linux/init.h>
> +#include <linux/io.h>
> +#include <linux/tpm.h>
> +
> +#define PCR5_INDEX 5
> +
> +unsigned long __initdata efi_pcr5_log = EFI_INVALID_TABLE_ADDR;
> +bool kernel_tpm_security_available __ro_after_init;
> +
> +static int __init efi_tpm_check_pcr5(void)
> +{
> + struct linux_efi_pcr5_log *log;
> + unsigned int recorded = 0, changed = 0;
> + struct tpm_chip *chip;
> + size_t size;
> + u32 count;
> + int i, rc;
> +
> + if (efi_pcr5_log == EFI_INVALID_TABLE_ADDR)
> + return 0;
> +
> + log = memremap(efi_pcr5_log, sizeof(*log), MEMREMAP_WB);
> + if (!log) {
> + pr_err("Failed to map PCR 5 log\n");
> + return 0;
> + }
> + count = log->count;
> + memunmap(log);
> +
> + size = struct_size(log, digests, count);
> + log = memremap(efi_pcr5_log, size, MEMREMAP_WB);
> + if (!log) {
> + pr_err("Failed to map PCR 5 log\n");
> + return 0;
> + }
> +
> + chip = tpm_default_chip();
> + if (!chip) {
> + pr_warn("No TPM available to verify PCR 5\n");
> + goto out;
> + }
> +
> + if (!tpm_is_tpm2(chip)) {
> + pr_warn("PCR 5 log requires a TPM 2.0\n");
> + goto out_put;
> + }
> +
> + for (i = 0; i < count; i++) {
> + struct linux_efi_pcr5_digest *d = &log->digests[i];
> + struct tpm_digest digest = { .alg_id = d->hash_alg };
> +
> + if (d->digest_size > sizeof(d->digest)) {
> + pr_err("Invalid PCR 5 log entry for bank 0x%04x\n",
> + d->hash_alg);
> + continue;
> + }
> +
> + rc = tpm_pcr_read(chip, PCR5_INDEX, &digest);
> + if (rc) {
> + pr_err("Failed to read PCR 5 bank 0x%04x: %d\n",
> + d->hash_alg, rc);
> + continue;
> + }
> +
> + recorded++;
> + if (memcmp(digest.digest, d->digest, d->digest_size))
> + changed++;
> + }
> +
> + if (!recorded)
> + goto out_put;
> +
> + if (!changed)
> + pr_err(FW_BUG "Firmware failed to extend PCR 5 for ExitBootServices\n");
> + else if (changed != recorded)
> + pr_err(FW_BUG "Firmware only extended one PCR bank in ExitBootServices\n");
> +
> + if (changed)
> + kernel_tpm_security_available = true;
> +
> +out_put:
> + put_device(&chip->dev);
> +out:
> + memunmap(log);
> + return 0;
> +}
> +late_initcall(efi_tpm_check_pcr5);
> diff --git a/include/linux/efi.h b/include/linux/efi.h
> index aa15ff88539b..6d51a4bffbd8 100644
> --- a/include/linux/efi.h
> +++ b/include/linux/efi.h
> @@ -23,6 +23,7 @@
> #include <linux/pstore.h>
> #include <linux/range.h>
> #include <linux/reboot.h>
> +#include <linux/tpm_command.h>
> #include <linux/uuid.h>
>
> #include <asm/page.h>
> @@ -422,6 +423,7 @@ void efi_native_runtime_setup(void);
> #define LINUX_EFI_COCO_SECRET_AREA_GUID EFI_GUID(0xadf956ad, 0xe98c, 0x484c, 0xae, 0x11, 0xb5, 0x1c, 0x7d, 0x33, 0x64, 0x47)
> #define LINUX_EFI_BOOT_MEMMAP_GUID EFI_GUID(0x800f683f, 0xd08b, 0x423a, 0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4)
> #define LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID EFI_GUID(0xd5d1de3c, 0x105c, 0x44f9, 0x9e, 0xa9, 0xbc, 0xef, 0x98, 0x12, 0x00, 0x31)
> +#define LINUX_EFI_PCR5_LOG_GUID EFI_GUID(0xb38f9ff0, 0xb8ef, 0xe28f, 0x80, 0x8d, 0xe2, 0x9a, 0xa7, 0xef, 0xb8, 0x8f)
>
> #define RISCV_EFI_BOOT_PROTOCOL_GUID EFI_GUID(0xccd15fec, 0x6f73, 0x4eec, 0x83, 0x95, 0x3e, 0x69, 0xe4, 0xb9, 0x40, 0xbf)
>
> @@ -631,6 +633,28 @@ typedef struct {
>
> extern unsigned long __ro_after_init efi_rng_seed; /* RNG Seed table */
>
> +/*
> + * The value of PCR 5 as read by the EFI stub immediately before calling
> + * ExitBootServices(), published via the LINUX_EFI_PCR5_LOG_GUID
> + * configuration table. There is one entry for each supported PCR bank
> + * that was active.
> + */
> +struct linux_efi_pcr5_digest {
> + u16 hash_alg; /* TPM_ALG_* of the PCR bank */
> + u16 digest_size;
> + u8 digest[TPM2_MAX_DIGEST_SIZE];
> +};
> +
> +struct linux_efi_pcr5_log {
> + u32 count;
> + struct linux_efi_pcr5_digest digests[];
> +};
> +
> +#ifdef CONFIG_KERNEL_TPM_SECURITY
> +extern unsigned long efi_pcr5_log;
> +extern bool kernel_tpm_security_available;
> +#endif
> +
> /*
> * All runtime access to EFI goes through this structure:
> */
> --
> 2.43.0
>
>
It looks correct and that is good enough at this point of time.
Ross, you might want to skim this (not sure, just in case).
Br, Jarkko
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH 03/17] tpm: Allow tpm2_start_auth_session() to start an audit session
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
2026-10-08 13:20 ` [PATCH 01/17] tpm: Define a kernel-owned TPM NV index that can't be modified by userland Matthew Garrett
2026-10-08 13:20 ` [PATCH 02/17] efi: Add a mechanism to modify TPM state depending on kernel security features Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 04/17] tpm: Log commands executed in " Matthew Garrett
` (14 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add an audit argument to tpm2_start_auth_session(). When set, the
session is flagged so that tpm_buf_append_hmac_session() sets
TPM2_SA_AUDIT (TPMA_SESSION_AUDIT) on every command using it, causing
the TPM to extend the session audit digest with each command and
response.
Since only a single session is held per chip, return -EBUSY if a
session is already active with a different audit setting rather than
silently reusing it.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm2-cmd.c | 4 +--
drivers/char/tpm/tpm2-sessions.c | 30 ++++++++++++++++++-----
include/linux/tpm.h | 5 ++--
security/keys/trusted-keys/trusted_tpm2.c | 6 ++---
4 files changed, 32 insertions(+), 13 deletions(-)
diff --git a/drivers/char/tpm/tpm2-cmd.c b/drivers/char/tpm/tpm2-cmd.c
index ae22295df798..bfad86ed0132 100644
--- a/drivers/char/tpm/tpm2-cmd.c
+++ b/drivers/char/tpm/tpm2-cmd.c
@@ -178,7 +178,7 @@ int tpm2_pcr_extend(struct tpm_chip *chip, u32 pcr_idx,
int i;
if (!disable_pcr_integrity) {
- rc = tpm2_start_auth_session(chip);
+ rc = tpm2_start_auth_session(chip, false);
if (rc)
return rc;
}
@@ -254,7 +254,7 @@ int tpm2_get_random(struct tpm_chip *chip, u8 *dest, size_t max)
if (!num_bytes || max > TPM_MAX_RNG_DATA)
return -EINVAL;
- err = tpm2_start_auth_session(chip);
+ err = tpm2_start_auth_session(chip, false);
if (err)
return err;
diff --git a/drivers/char/tpm/tpm2-sessions.c b/drivers/char/tpm/tpm2-sessions.c
index ca1e2bf424e1..9fb710a1c6c6 100644
--- a/drivers/char/tpm/tpm2-sessions.c
+++ b/drivers/char/tpm/tpm2-sessions.c
@@ -129,6 +129,8 @@ struct tpm2_auth {
struct aes_enckey aes_key;
/* saved session attributes: */
u8 attrs;
+ /* set TPM2_SA_AUDIT on every command using this session */
+ bool audit;
__be32 ordinal;
/*
@@ -376,6 +378,10 @@ void tpm_buf_append_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf,
/* The first write to /dev/tpm{rm0} will flush the session. */
attributes |= TPM2_SA_CONTINUE_SESSION;
+ auth = chip->auth;
+ if (auth->audit)
+ attributes |= TPM2_SA_AUDIT;
+
/*
* The Architecture Guide requires us to strip trailing zeros
* before computing the HMAC
@@ -383,7 +389,6 @@ void tpm_buf_append_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf,
while (passphrase && passphrase_len > 0 && passphrase[passphrase_len - 1] == '\0')
passphrase_len--;
- auth = chip->auth;
auth->attrs = attributes;
auth->passphrase_len = passphrase_len;
if (passphrase_len)
@@ -777,6 +782,7 @@ int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
off_t offset_s, offset_p;
u8 rphash[SHA256_DIGEST_SIZE];
u32 attrs, cc;
+ u8 rsp_attrs;
struct sha256_ctx sctx;
struct hmac_sha256_ctx hctx;
u16 tag = be16_to_cpu(head->tag);
@@ -828,7 +834,7 @@ int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
goto out;
memcpy(auth->tpm_nonce, &buf->data[offset_s], len);
offset_s += len;
- attrs = tpm_buf_read_u8(buf, &offset_s);
+ rsp_attrs = tpm_buf_read_u8(buf, &offset_s);
len = tpm_buf_read_u16(buf, &offset_s);
if (offset_s + len != tpm_buf_length(buf))
goto out;
@@ -854,7 +860,12 @@ int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
hmac_sha256_update(&hctx, rphash, sizeof(rphash));
hmac_sha256_update(&hctx, auth->tpm_nonce, sizeof(auth->tpm_nonce));
hmac_sha256_update(&hctx, auth->our_nonce, sizeof(auth->our_nonce));
- hmac_sha256_update(&hctx, &auth->attrs, 1);
+ /*
+ * The response HMAC covers the response's session attributes, which
+ * can differ from the command's - the TPM sets auditExclusive in the
+ * response to an exclusive audit session.
+ */
+ hmac_sha256_update(&hctx, &rsp_attrs, 1);
/* we're done with the rphash, so put our idea of the hmac there */
hmac_sha256_final(&hctx, rphash);
if (crypto_memneq(rphash, &buf->data[offset_s], SHA256_DIGEST_SIZE)) {
@@ -988,13 +999,17 @@ static int tpm2_load_null(struct tpm_chip *chip, u32 *null_key)
/**
* tpm2_start_auth_session() - Create an a HMAC authentication session
* @chip: A TPM chip
+ * @audit: Set TPM2_SA_AUDIT on every command using the session
*
* Loads the ephemeral key (null seed), and starts an HMAC authenticated
- * session. The null seed is flushed before the return.
+ * session. The null seed is flushed before the return. If @audit is
+ * true, the session is used as an audit session and the TPM will
+ * extend the session digest with each command and response.
*
- * Returns zero on success, or a POSIX error code.
+ * Returns zero on success, -EBUSY if a session with a different audit
+ * setting is already active, or a POSIX error code.
*/
-int tpm2_start_auth_session(struct tpm_chip *chip)
+int tpm2_start_auth_session(struct tpm_chip *chip, bool audit)
{
struct tpm_buf *buf __free(kfree) = NULL;
struct tpm2_auth *auth;
@@ -1002,6 +1017,8 @@ int tpm2_start_auth_session(struct tpm_chip *chip)
int rc;
if (chip->auth) {
+ if (chip->auth->audit != audit)
+ return -EBUSY;
dev_dbg_once(&chip->dev, "auth session is active\n");
return 0;
}
@@ -1009,6 +1026,7 @@ int tpm2_start_auth_session(struct tpm_chip *chip)
auth = kzalloc_obj(*auth);
if (!auth)
return -ENOMEM;
+ auth->audit = audit;
rc = tpm2_load_null(chip, &null_key);
if (rc) {
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index b6b862c3be3b..341fd5b46b2d 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -321,7 +321,7 @@ void tpm_buf_append_auth(struct tpm_chip *chip, struct tpm_buf *buf,
#ifdef CONFIG_TCG_TPM2_HMAC
-int tpm2_start_auth_session(struct tpm_chip *chip);
+int tpm2_start_auth_session(struct tpm_chip *chip, bool audit);
int tpm_buf_fill_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf);
int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
int rc);
@@ -329,7 +329,8 @@ void tpm2_end_auth_session(struct tpm_chip *chip);
#else
#include <linux/unaligned.h>
-static inline int tpm2_start_auth_session(struct tpm_chip *chip)
+static inline int tpm2_start_auth_session(struct tpm_chip *chip,
+ bool audit)
{
return 0;
}
diff --git a/security/keys/trusted-keys/trusted_tpm2.c b/security/keys/trusted-keys/trusted_tpm2.c
index 01f18bb37047..8a15bf542a9e 100644
--- a/security/keys/trusted-keys/trusted_tpm2.c
+++ b/security/keys/trusted-keys/trusted_tpm2.c
@@ -252,7 +252,7 @@ int tpm2_seal_trusted(struct tpm_chip *chip,
if (rc)
return rc;
- rc = tpm2_start_auth_session(chip);
+ rc = tpm2_start_auth_session(chip, false);
if (rc)
goto out_put;
@@ -428,7 +428,7 @@ static int tpm2_load_cmd(struct tpm_chip *chip,
if (blob_len > payload->blob_len)
return -E2BIG;
- rc = tpm2_start_auth_session(chip);
+ rc = tpm2_start_auth_session(chip, false);
if (rc)
return rc;
@@ -492,7 +492,7 @@ static int tpm2_unseal_cmd(struct tpm_chip *chip,
u8 *data;
int rc;
- rc = tpm2_start_auth_session(chip);
+ rc = tpm2_start_auth_session(chip, false);
if (rc)
return rc;
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 04/17] tpm: Log commands executed in an audit session
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (2 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 03/17] tpm: Allow tpm2_start_auth_session() to start an audit session Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval Matthew Garrett
` (13 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Verifying a session audit digest requires knowing every command that
was executed in the session. The TPM extends the audit digest for
each successful command as
digest_new := H(digest_old || cpHash || rpHash)
so the cpHash and rpHash of each command are all that is needed to
recompute it. Both are already calculated for HMAC generation and
verification, so record them in a per-session log when the session is
an audit session. Add some helpers to retrieve and free the log.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm-chip.c | 2 +-
drivers/char/tpm/tpm.h | 1 +
drivers/char/tpm/tpm2-sessions.c | 98 +++++++++++++++++++++++++++++++-
include/linux/tpm.h | 17 ++++++
4 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/drivers/char/tpm/tpm-chip.c b/drivers/char/tpm/tpm-chip.c
index 12b7394b34bd..cb10f2d1eace 100644
--- a/drivers/char/tpm/tpm-chip.c
+++ b/drivers/char/tpm/tpm-chip.c
@@ -247,7 +247,7 @@ static void tpm_dev_release(struct device *dev)
kfree(chip->work_space.context_buf);
kfree(chip->work_space.session_buf);
#ifdef CONFIG_TCG_TPM2_HMAC
- kfree_sensitive(chip->auth);
+ tpm2_free_auth(chip->auth);
#endif
kfree(chip);
}
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index fa554c5ad80b..e55fa22a13eb 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -144,6 +144,7 @@ void tpm_dev_common_exit(void);
#ifdef CONFIG_TCG_TPM2_HMAC
int tpm2_sessions_init(struct tpm_chip *chip);
+void tpm2_free_auth(struct tpm2_auth *auth);
#else
static inline int tpm2_sessions_init(struct tpm_chip *chip)
{
diff --git a/drivers/char/tpm/tpm2-sessions.c b/drivers/char/tpm/tpm2-sessions.c
index 9fb710a1c6c6..56323a9ac87a 100644
--- a/drivers/char/tpm/tpm2-sessions.c
+++ b/drivers/char/tpm/tpm2-sessions.c
@@ -131,6 +131,13 @@ struct tpm2_auth {
u8 attrs;
/* set TPM2_SA_AUDIT on every command using this session */
bool audit;
+ /*
+ * Log of every successfully executed command in an audit
+ * session.
+ */
+ struct tpm2_audit_entry *audit_log;
+ unsigned int audit_log_len;
+ unsigned int audit_log_size;
__be32 ordinal;
/*
@@ -143,6 +150,42 @@ struct tpm2_auth {
};
#ifdef CONFIG_TCG_TPM2_HMAC
+void tpm2_free_auth(struct tpm2_auth *auth)
+{
+ if (!auth)
+ return;
+
+ kfree(auth->audit_log);
+ kfree_sensitive(auth);
+}
+
+/*
+ * Make room for the entry describing the command about to be sent.
+ */
+static int tpm2_audit_log_reserve(struct tpm2_auth *auth)
+{
+ struct tpm2_audit_entry *log;
+ unsigned int size;
+
+ if (auth->audit_log_len < auth->audit_log_size)
+ return 0;
+
+ size = auth->audit_log_size ? auth->audit_log_size * 2 : 8;
+ log = kcalloc(size, sizeof(*log), GFP_KERNEL);
+ if (!log)
+ return -ENOMEM;
+
+ if (auth->audit_log) {
+ memcpy(log, auth->audit_log,
+ auth->audit_log_len * sizeof(*log));
+ kfree_sensitive(auth->audit_log);
+ }
+ auth->audit_log = log;
+ auth->audit_log_size = size;
+
+ return 0;
+}
+
/*
* Name Size based on TPM algorithm (assumes no hash bigger than 255)
*/
@@ -730,6 +773,15 @@ int tpm_buf_fill_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf)
tpm_buf_length(buf) - offset_s);
sha256_final(&sctx, cphash);
+ if (auth->audit) {
+ ret = tpm2_audit_log_reserve(auth);
+ if (ret)
+ goto err;
+
+ memcpy(auth->audit_log[auth->audit_log_len].cphash, cphash,
+ sizeof(cphash));
+ }
+
/* now calculate the hmac */
hmac_sha256_init_usingrawkey(&hctx, auth->session_key,
sizeof(auth->session_key) +
@@ -853,6 +905,18 @@ int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
sha256_update(&sctx, &buf->data[offset_p], parm_len);
sha256_final(&sctx, rphash);
+ /*
+ * The TPM extends the audit digest for every successful
+ * command, so log it even if the HMAC check below fails: a
+ * tampered response will then show up as an audit digest
+ * mismatch.
+ */
+ if (auth->audit) {
+ memcpy(auth->audit_log[auth->audit_log_len].rphash, rphash,
+ sizeof(rphash));
+ auth->audit_log_len++;
+ }
+
/* now calculate the hmac */
hmac_sha256_init_usingrawkey(&hctx, auth->session_key,
sizeof(auth->session_key) +
@@ -895,7 +959,7 @@ int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
/* manually close the session if it wasn't consumed */
tpm2_flush_context(chip, auth->handle);
- kfree_sensitive(auth);
+ tpm2_free_auth(auth);
chip->auth = NULL;
} else {
/* reset for next use */
@@ -924,11 +988,41 @@ void tpm2_end_auth_session(struct tpm_chip *chip)
return;
tpm2_flush_context(chip, auth->handle);
- kfree_sensitive(auth);
+ tpm2_free_auth(auth);
chip->auth = NULL;
}
EXPORT_SYMBOL(tpm2_end_auth_session);
+/**
+ * tpm2_get_audit_log() - retrieve the log of an audit session
+ * @chip: the TPM chip structure
+ * @log: set to the array of log entries
+ *
+ * Each entry holds the cpHash and rpHash of one successfully executed
+ * command, in the order in which they were executed. This is the
+ * information needed to recompute the session audit digest:
+ *
+ * digest_new := SHA256(digest_old || cpHash || rpHash)
+ *
+ * The log is owned by the session and is only valid until the session
+ * is ended or another command is sent using it.
+ *
+ * Returns: the number of entries in the log, or -EINVAL if there is no
+ * active audit session.
+ */
+int tpm2_get_audit_log(struct tpm_chip *chip,
+ const struct tpm2_audit_entry **log)
+{
+ struct tpm2_auth *auth = chip->auth;
+
+ if (!auth || !auth->audit)
+ return -EINVAL;
+
+ *log = auth->audit_log;
+ return auth->audit_log_len;
+}
+EXPORT_SYMBOL(tpm2_get_audit_log);
+
static int tpm2_parse_start_auth_session(struct tpm2_auth *auth,
struct tpm_buf *buf)
{
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 341fd5b46b2d..c1d0617ff8a1 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -319,9 +319,21 @@ void tpm_buf_append_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf,
void tpm_buf_append_auth(struct tpm_chip *chip, struct tpm_buf *buf,
u8 *passphrase, int passphraselen);
+/**
+ * struct tpm2_audit_entry - a command executed in an audit session
+ * @cphash: SHA256 command parameter hash (cpHash)
+ * @rphash: SHA256 response parameter hash (rpHash)
+ */
+struct tpm2_audit_entry {
+ u8 cphash[SHA256_DIGEST_SIZE];
+ u8 rphash[SHA256_DIGEST_SIZE];
+};
+
#ifdef CONFIG_TCG_TPM2_HMAC
int tpm2_start_auth_session(struct tpm_chip *chip, bool audit);
+int tpm2_get_audit_log(struct tpm_chip *chip,
+ const struct tpm2_audit_entry **log);
int tpm_buf_fill_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf);
int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
int rc);
@@ -337,6 +349,11 @@ static inline int tpm2_start_auth_session(struct tpm_chip *chip,
static inline void tpm2_end_auth_session(struct tpm_chip *chip)
{
}
+static inline int tpm2_get_audit_log(struct tpm_chip *chip,
+ const struct tpm2_audit_entry **log)
+{
+ return -EOPNOTSUPP;
+}
static inline int tpm_buf_fill_hmac_session(struct tpm_chip *chip,
struct tpm_buf *buf)
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (3 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 04/17] tpm: Log commands executed in " Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 16:45 ` James Bottomley
2026-10-08 13:20 ` [PATCH 06/17] tpm: Use TPM2_NV_ReadPublic to read NV index names Matthew Garrett
` (12 subsequent siblings)
17 siblings, 1 reply; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm2_create_kernel_ak(), which creates a restricted ECDSA P-256
signing key as a primary key in the owner hierarchy. The template's
unique field holds a fixed seed, which means that (since the primary
keys are derived from the seed and template) the same AK is generated on
every call until something changes the owner seed (ie, the TPM being
cleared or replaced).
We can use this to get a signed copy of the audit digest from a TPM
audit session. Add tpm2_get_signed_audit_digest(), which uses the AK to
sign the digest of the active audit session and returns the TPMS_ATTEST
structure, the signature and the AK public key.
Both the owner and endorsement hierarchies are assumed to have empty
auth values, which is the default. If this turns out to be a problem in
the real world we can look at providing a mechanism for userland to
provide the values at boot or resume times.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/Makefile | 1 +
drivers/char/tpm/tpm.h | 1 +
drivers/char/tpm/tpm2-ak.c | 374 +++++++++++++++++++++++++++++++
drivers/char/tpm/tpm2-sessions.c | 15 ++
include/linux/tpm.h | 38 ++++
include/linux/tpm_command.h | 8 +
6 files changed, 437 insertions(+)
create mode 100644 drivers/char/tpm/tpm2-ak.c
diff --git a/drivers/char/tpm/Makefile b/drivers/char/tpm/Makefile
index 5b5cdc0d32e4..10a4ed388dea 100644
--- a/drivers/char/tpm/Makefile
+++ b/drivers/char/tpm/Makefile
@@ -17,6 +17,7 @@ tpm-y += eventlog/tpm1.o
tpm-y += eventlog/tpm2.o
tpm-y += tpm-buf.o
tpm-y += tpm2-sessions.o
+tpm-$(CONFIG_TCG_TPM2_HMAC) += tpm2-ak.o
tpm-$(CONFIG_ACPI) += tpm_ppi.o eventlog/acpi.o
tpm-$(CONFIG_EFI) += eventlog/efi.o
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index e55fa22a13eb..23070bdb2aa4 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -145,6 +145,7 @@ void tpm_dev_common_exit(void);
#ifdef CONFIG_TCG_TPM2_HMAC
int tpm2_sessions_init(struct tpm_chip *chip);
void tpm2_free_auth(struct tpm2_auth *auth);
+int tpm2_audit_session_handle(struct tpm_chip *chip, u32 *handle);
#else
static inline int tpm2_sessions_init(struct tpm_chip *chip)
{
diff --git a/drivers/char/tpm/tpm2-ak.c b/drivers/char/tpm/tpm2-ak.c
new file mode 100644
index 000000000000..ad24d36b1728
--- /dev/null
+++ b/drivers/char/tpm/tpm2-ak.c
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Kernel attestation key (AK) support.
+ *
+ * The kernel AK is a restricted ECDSA P-256 signing key created as a
+ * primary key in the owner hierarchy from a fixed template. Primary keys
+ * are derived from the hierarchy seed and the template, so on a given TPM
+ * the same AK is generated every time until the owner seed changes, which
+ * happens when the TPM is cleared.
+ *
+ * The AK can be used to sign the digest of the kernel's audit session,
+ * allowing the log returned by tpm2_get_audit_log() to be verified.
+ */
+
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include "tpm.h"
+
+/*
+ * Restricted signing key whose private part never leaves the TPM. NO_DA
+ * as the key has an empty auth value.
+ */
+#define TPM2_OA_KERNEL_AK ( \
+ TPM2_OA_FIXED_TPM | \
+ TPM2_OA_FIXED_PARENT | \
+ TPM2_OA_SENSITIVE_DATA_ORIGIN | \
+ TPM2_OA_USER_WITH_AUTH | \
+ TPM2_OA_NO_DA | \
+ TPM2_OA_RESTRICTED | \
+ TPM2_OA_SIGN)
+
+/*
+ * Placed in the unique field of the template. Changing it changes the
+ * AK derived from the owner seed.
+ */
+static const u8 tpm2_kernel_ak_seed[EC_PT_SZ] =
+ "Linux kernel attestation key v1";
+
+/* Bounds-checked reader for TPM response data */
+struct tpm2_rsp {
+ const u8 *data;
+ u32 len;
+ u32 off;
+ bool err;
+};
+
+static const u8 *tpm2_rsp_bytes(struct tpm2_rsp *r, u32 count)
+{
+ const u8 *p;
+
+ if (r->err || r->len - r->off < count) {
+ r->err = true;
+ return NULL;
+ }
+
+ p = &r->data[r->off];
+ r->off += count;
+ return p;
+}
+
+static u16 tpm2_rsp_u16(struct tpm2_rsp *r)
+{
+ const u8 *p = tpm2_rsp_bytes(r, sizeof(u16));
+
+ return p ? get_unaligned_be16(p) : 0;
+}
+
+static u32 tpm2_rsp_u32(struct tpm2_rsp *r)
+{
+ const u8 *p = tpm2_rsp_bytes(r, sizeof(u32));
+
+ return p ? get_unaligned_be32(p) : 0;
+}
+
+/* Initialise a reader over the response held in @buf */
+static void tpm2_rsp_init(struct tpm2_rsp *r, struct tpm_buf *buf)
+{
+ struct tpm_header *head = (struct tpm_header *)buf->data;
+
+ r->data = buf->data;
+ r->len = min_t(u32, be32_to_cpu(head->length), TPM_BUFSIZE);
+ r->off = TPM_HEADER_SIZE;
+ r->err = r->len < TPM_HEADER_SIZE;
+}
+
+/* Read a TPM2B_ECC_PARAMETER, left-padding it to EC_PT_SZ bytes */
+static void tpm2_rsp_ecc_param(struct tpm2_rsp *r, u8 *out)
+{
+ u16 len = tpm2_rsp_u16(r);
+ const u8 *p;
+
+ if (len > EC_PT_SZ) {
+ r->err = true;
+ return;
+ }
+
+ p = tpm2_rsp_bytes(r, len);
+ if (!p)
+ return;
+
+ memset(out, 0, EC_PT_SZ - len);
+ memcpy(out + EC_PT_SZ - len, p, len);
+}
+
+/* Parse and validate the TPM2B_PUBLIC of the kernel AK */
+static int tpm2_parse_kernel_ak_public(struct tpm2_rsp *r, u8 *x, u8 *y)
+{
+ u16 size = tpm2_rsp_u16(r);
+ u32 end = r->off + size;
+
+ if (tpm2_rsp_u16(r) != TPM_ALG_ECC ||
+ tpm2_rsp_u16(r) != TPM_ALG_SHA256 ||
+ tpm2_rsp_u32(r) != TPM2_OA_KERNEL_AK ||
+ tpm2_rsp_u16(r) != 0 || /* authPolicy */
+ tpm2_rsp_u16(r) != TPM_ALG_NULL || /* symmetric */
+ tpm2_rsp_u16(r) != TPM_ALG_ECDSA || /* scheme */
+ tpm2_rsp_u16(r) != TPM_ALG_SHA256 || /* scheme hash */
+ tpm2_rsp_u16(r) != TPM2_ECC_NIST_P256 ||
+ tpm2_rsp_u16(r) != TPM_ALG_NULL) /* kdf */
+ return -EINVAL;
+
+ tpm2_rsp_ecc_param(r, x);
+ tpm2_rsp_ecc_param(r, y);
+
+ if (r->err || r->off != end)
+ return -EINVAL;
+
+ return 0;
+}
+
+/**
+ * tpm2_create_kernel_ak() - create the kernel attestation key
+ * @chip: the TPM chip
+ * @handle: set to the transient handle of the AK on success
+ * @x: if not NULL, filled with the X coordinate of the AK public key
+ * @y: if not NULL, filled with the Y coordinate of the AK public key
+ *
+ * Creates the kernel AK as a primary key in the owner hierarchy using a
+ * fixed template, so the same key is returned on every call until the
+ * TPM is cleared. The owner hierarchy must have an empty auth value.
+ * The caller must hold the chip's ops lock and is responsible for
+ * flushing @handle with tpm2_flush_context().
+ *
+ * Return:
+ * * 0 - OK
+ * * -errno - A system error
+ * * TPM_RC - A TPM error
+ */
+int tpm2_create_kernel_ak(struct tpm_chip *chip, u32 *handle, u8 *x, u8 *y)
+{
+ struct tpm_buf *template __free(kfree) = NULL;
+ struct tpm_buf *buf __free(kfree) = NULL;
+ u8 ak_x[EC_PT_SZ], ak_y[EC_PT_SZ];
+ struct tpm2_rsp r;
+ u32 ak;
+ int rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ template = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!template)
+ return -ENOMEM;
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_CREATE_PRIMARY);
+ tpm_buf_init_sized(template, TPM_BUFSIZE);
+
+ /* key type */
+ tpm_buf_append_u16(template, TPM_ALG_ECC);
+ /* name algorithm */
+ tpm_buf_append_u16(template, TPM_ALG_SHA256);
+ /* object properties */
+ tpm_buf_append_u32(template, TPM2_OA_KERNEL_AK);
+ /* auth policy (empty) */
+ tpm_buf_append_u16(template, 0);
+ /* symmetric algorithm (none for a signing key) */
+ tpm_buf_append_u16(template, TPM_ALG_NULL);
+ /* signing scheme */
+ tpm_buf_append_u16(template, TPM_ALG_ECDSA);
+ tpm_buf_append_u16(template, TPM_ALG_SHA256);
+ /* ECC curve */
+ tpm_buf_append_u16(template, TPM2_ECC_NIST_P256);
+ /* KDF scheme */
+ tpm_buf_append_u16(template, TPM_ALG_NULL);
+ /* unique: the fixed seed as X, empty Y */
+ tpm_buf_append_u16(template, sizeof(tpm2_kernel_ak_seed));
+ tpm_buf_append(template, tpm2_kernel_ak_seed,
+ sizeof(tpm2_kernel_ak_seed));
+ tpm_buf_append_u16(template, 0);
+
+ /* primary handle */
+ tpm_buf_append_handle(buf, TPM2_RH_OWNER);
+ tpm_buf_append_auth(chip, buf, NULL, 0);
+
+ /* sensitive create: empty auth and data */
+ tpm_buf_append_u16(buf, 4);
+ tpm_buf_append_u16(buf, 0);
+ tpm_buf_append_u16(buf, 0);
+
+ /* the public template */
+ tpm_buf_append(buf, template->data, template->length);
+
+ /* outside info (empty) */
+ tpm_buf_append_u16(buf, 0);
+
+ /* creation PCR (none) */
+ tpm_buf_append_u32(buf, 0);
+
+ if (buf->flags & TPM_BUF_INVALID || template->flags & TPM_BUF_INVALID)
+ return -EINVAL;
+
+ rc = tpm_transmit_cmd(chip, buf, 0, "creating kernel AK");
+ if (rc)
+ return rc;
+
+ tpm2_rsp_init(&r, buf);
+ ak = tpm2_rsp_u32(&r);
+ /* parameterSize */
+ tpm2_rsp_u32(&r);
+ if (r.err)
+ return -EINVAL;
+
+ rc = tpm2_parse_kernel_ak_public(&r, ak_x, ak_y);
+ if (rc) {
+ dev_err(&chip->dev, "unexpected kernel AK public area\n");
+ tpm2_flush_context(chip, ak);
+ return rc;
+ }
+
+ if (x)
+ memcpy(x, ak_x, EC_PT_SZ);
+ if (y)
+ memcpy(y, ak_y, EC_PT_SZ);
+ *handle = ak;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(tpm2_create_kernel_ak);
+
+/**
+ * tpm2_get_signed_audit_digest() - get the audit session digest signed by the AK
+ * @chip: the TPM chip
+ * @nonce: qualifying data to include in the attestation (may be NULL)
+ * @nonce_len: length of @nonce
+ * @audit: filled with the signed attestation and the AK public key
+ *
+ * Creates the kernel AK and uses TPM2_GetSessionAuditDigest to have it
+ * sign the digest of the active audit session. The session itself is not
+ * used to authorize the command, so the audit digest and the log returned
+ * by tpm2_get_audit_log() are unaffected. The endorsement and owner
+ * hierarchies must have empty auth values. The caller must hold the
+ * chip's ops lock and must release @audit with tpm2_free_signed_audit().
+ *
+ * Return:
+ * * 0 - OK
+ * * -EINVAL - No audit session is active, or the response was malformed
+ * * -errno - A system error
+ * * TPM_RC - A TPM error
+ */
+int tpm2_get_signed_audit_digest(struct tpm_chip *chip, const u8 *nonce,
+ u16 nonce_len,
+ struct tpm2_signed_audit *audit)
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ u32 session, ak, param_size, end;
+ const u8 *attest;
+ struct tpm2_rsp r;
+ u16 attest_len;
+ int rc;
+
+ memset(audit, 0, sizeof(*audit));
+
+ if (nonce_len > TPM2_MAX_DIGEST_SIZE)
+ return -EINVAL;
+
+ rc = tpm2_audit_session_handle(chip, &session);
+ if (rc)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ rc = tpm2_create_kernel_ak(chip, &ak, audit->ak_x, audit->ak_y);
+ if (rc)
+ return rc;
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_GET_SESSION_AUDIT_DIGEST);
+
+ /* privacyAdminHandle, signHandle, sessionHandle */
+ tpm_buf_append_handle(buf, TPM2_RH_ENDORSEMENT);
+ tpm_buf_append_handle(buf, ak);
+ tpm_buf_append_handle(buf, session);
+
+ /* empty password authorizations for the endorsement hierarchy and AK */
+ tpm_buf_append_auth(chip, buf, NULL, 0);
+ tpm_buf_append_auth(chip, buf, NULL, 0);
+
+ /* qualifyingData */
+ tpm_buf_append_u16(buf, nonce_len);
+ if (nonce_len)
+ tpm_buf_append(buf, nonce, nonce_len);
+
+ /* inScheme: use the AK's scheme */
+ tpm_buf_append_u16(buf, TPM_ALG_NULL);
+
+ if (buf->flags & TPM_BUF_INVALID) {
+ rc = -EINVAL;
+ goto out;
+ }
+
+ rc = tpm_transmit_cmd(chip, buf, 0, "getting session audit digest");
+ if (rc)
+ goto out;
+
+ rc = -EINVAL;
+ tpm2_rsp_init(&r, buf);
+ param_size = tpm2_rsp_u32(&r);
+ end = r.off + param_size;
+
+ /* auditInfo */
+ attest_len = tpm2_rsp_u16(&r);
+ attest = tpm2_rsp_bytes(&r, attest_len);
+ if (!attest)
+ goto out;
+
+ /* TPMS_ATTEST begins with magic and type */
+ if (attest_len < sizeof(u32) + sizeof(u16) ||
+ get_unaligned_be32(attest) != TPM2_GENERATED_VALUE ||
+ get_unaligned_be16(attest + sizeof(u32)) !=
+ TPM2_ST_ATTEST_SESSION_AUDIT)
+ goto out;
+
+ /* signature */
+ if (tpm2_rsp_u16(&r) != TPM_ALG_ECDSA ||
+ tpm2_rsp_u16(&r) != TPM_ALG_SHA256)
+ goto out;
+ tpm2_rsp_ecc_param(&r, audit->sig_r);
+ tpm2_rsp_ecc_param(&r, audit->sig_s);
+
+ if (r.err || r.off != end)
+ goto out;
+
+ audit->attest = kmemdup(attest, attest_len, GFP_KERNEL);
+ if (!audit->attest) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ audit->attest_len = attest_len;
+ rc = 0;
+
+out:
+ if (rc < 0 && rc != -ENOMEM)
+ dev_err(&chip->dev, "failed to get session audit digest: %d\n",
+ rc);
+ tpm2_flush_context(chip, ak);
+ return rc;
+}
+EXPORT_SYMBOL_GPL(tpm2_get_signed_audit_digest);
+
+/**
+ * tpm2_free_signed_audit() - release a signed audit digest
+ * @audit: the structure filled by tpm2_get_signed_audit_digest()
+ */
+void tpm2_free_signed_audit(struct tpm2_signed_audit *audit)
+{
+ kfree(audit->attest);
+ audit->attest = NULL;
+ audit->attest_len = 0;
+}
+EXPORT_SYMBOL_GPL(tpm2_free_signed_audit);
diff --git a/drivers/char/tpm/tpm2-sessions.c b/drivers/char/tpm/tpm2-sessions.c
index 56323a9ac87a..badc9bec9caa 100644
--- a/drivers/char/tpm/tpm2-sessions.c
+++ b/drivers/char/tpm/tpm2-sessions.c
@@ -1023,6 +1023,21 @@ int tpm2_get_audit_log(struct tpm_chip *chip,
}
EXPORT_SYMBOL(tpm2_get_audit_log);
+/*
+ * Return the handle of the active audit session, or -EINVAL if there is
+ * no active audit session.
+ */
+int tpm2_audit_session_handle(struct tpm_chip *chip, u32 *handle)
+{
+ struct tpm2_auth *auth = chip->auth;
+
+ if (!auth || !auth->audit)
+ return -EINVAL;
+
+ *handle = auth->handle;
+ return 0;
+}
+
static int tpm2_parse_start_auth_session(struct tpm2_auth *auth,
struct tpm_buf *buf)
{
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index c1d0617ff8a1..1d828b32847f 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -329,11 +329,35 @@ struct tpm2_audit_entry {
u8 rphash[SHA256_DIGEST_SIZE];
};
+/**
+ * struct tpm2_signed_audit - a session audit digest signed by the kernel AK
+ * @ak_x: X coordinate of the AK's P-256 public key
+ * @ak_y: Y coordinate of the AK's P-256 public key
+ * @attest: the signed TPMS_ATTEST structure, containing the audit digest
+ * @attest_len: length of @attest
+ * @sig_r: R component of the ECDSA-SHA256 signature over @attest
+ * @sig_s: S component of the ECDSA-SHA256 signature over @attest
+ */
+struct tpm2_signed_audit {
+ u8 ak_x[EC_PT_SZ];
+ u8 ak_y[EC_PT_SZ];
+ u8 *attest;
+ u16 attest_len;
+ u8 sig_r[EC_PT_SZ];
+ u8 sig_s[EC_PT_SZ];
+};
+
#ifdef CONFIG_TCG_TPM2_HMAC
int tpm2_start_auth_session(struct tpm_chip *chip, bool audit);
int tpm2_get_audit_log(struct tpm_chip *chip,
const struct tpm2_audit_entry **log);
+int tpm2_create_kernel_ak(struct tpm_chip *chip, u32 *handle,
+ u8 *x, u8 *y);
+int tpm2_get_signed_audit_digest(struct tpm_chip *chip, const u8 *nonce,
+ u16 nonce_len,
+ struct tpm2_signed_audit *audit);
+void tpm2_free_signed_audit(struct tpm2_signed_audit *audit);
int tpm_buf_fill_hmac_session(struct tpm_chip *chip, struct tpm_buf *buf);
int tpm_buf_check_hmac_response(struct tpm_chip *chip, struct tpm_buf *buf,
int rc);
@@ -354,6 +378,20 @@ static inline int tpm2_get_audit_log(struct tpm_chip *chip,
{
return -EOPNOTSUPP;
}
+static inline int tpm2_create_kernel_ak(struct tpm_chip *chip, u32 *handle,
+ u8 *x, u8 *y)
+{
+ return -EOPNOTSUPP;
+}
+static inline int
+tpm2_get_signed_audit_digest(struct tpm_chip *chip, const u8 *nonce,
+ u16 nonce_len, struct tpm2_signed_audit *audit)
+{
+ return -EOPNOTSUPP;
+}
+static inline void tpm2_free_signed_audit(struct tpm2_signed_audit *audit)
+{
+}
static inline int tpm_buf_fill_hmac_session(struct tpm_chip *chip,
struct tpm_buf *buf)
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index 79f547ca6dbf..486493efa759 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -189,6 +189,7 @@ enum tpm2_timeouts {
enum tpm2_structures {
TPM2_ST_NO_SESSIONS = 0x8001,
TPM2_ST_SESSIONS = 0x8002,
+ TPM2_ST_ATTEST_SESSION_AUDIT = 0x8016,
TPM2_ST_CREATION = 0x8021,
};
@@ -230,6 +231,7 @@ enum tpm2_command_codes {
TPM2_CC_SELF_TEST = 0x0143,
TPM2_CC_STARTUP = 0x0144,
TPM2_CC_SHUTDOWN = 0x0145,
+ TPM2_CC_GET_SESSION_AUDIT_DIGEST = 0x014D,
TPM2_CC_NV_READ = 0x014E,
TPM2_CC_NV_READ_LOCK = 0x014F,
TPM2_CC_CREATE = 0x0153,
@@ -275,10 +277,15 @@ enum tpm2_cc_attrs {
};
enum tpm2_permanent_handles {
+ TPM2_RH_OWNER = 0x40000001,
TPM2_RH_NULL = 0x40000007,
TPM2_RS_PW = 0x40000009,
+ TPM2_RH_ENDORSEMENT = 0x4000000B,
};
+/* TPMS_ATTEST.magic */
+#define TPM2_GENERATED_VALUE 0xff544347
+
/* Most Significant Octet for key types */
enum tpm2_mso_type {
TPM2_MSO_NVRAM = 0x01,
@@ -479,6 +486,7 @@ enum tpm_algorithms {
TPM_ALG_SHA512 = 0x000D,
TPM_ALG_NULL = 0x0010,
TPM_ALG_SM3_256 = 0x0012,
+ TPM_ALG_ECDSA = 0x0018,
TPM_ALG_ECC = 0x0023,
TPM_ALG_CFB = 0x0043,
};
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval
2026-10-08 13:20 ` [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval Matthew Garrett
@ 2026-10-08 16:45 ` James Bottomley
2026-10-09 8:29 ` Matthew Garrett
0 siblings, 1 reply; 31+ messages in thread
From: James Bottomley @ 2026-10-08 16:45 UTC (permalink / raw)
To: Matthew Garrett, mjg59
Cc: keyrings, linux-integrity, rafael, linux-pm, linux-efi
On Thu, 2026-10-08 at 06:20 -0700, Matthew Garrett wrote:
[...]
> +/**
> + * tpm2_create_kernel_ak() - create the kernel attestation key
> + * @chip: the TPM chip
> + * @handle: set to the transient handle of the AK on success
> + * @x: if not NULL, filled with the X coordinate of the AK
> public key
> + * @y: if not NULL, filled with the Y coordinate of the AK
> public key
> + *
> + * Creates the kernel AK as a primary key in the owner hierarchy
> using a
> + * fixed template, so the same key is returned on every call until
> the
> + * TPM is cleared. The owner hierarchy must have an empty auth
> value.
> + * The caller must hold the chip's ops lock and is responsible for
> + * flushing @handle with tpm2_flush_context().
> + *
> + * Return:
> + * * 0 - OK
> + * * -errno - A system error
> + * * TPM_RC - A TPM error
> + */
> +int tpm2_create_kernel_ak(struct tpm_chip *chip, u32 *handle, u8 *x,
> u8 *y)
Oof, there's a lot of duplication in here. Basically you're creating a
signing primary key (technically it's not an attestation key because
they're supposed to be non-primary; this isn't a criticism because the
only consumer is the machine owner who doesn't need to use privacy
preserving AKs because they know all about the TPM in their own
machine). The primary generation routines that exist in tpm2-
sessions.c:tpm2_create_primary() et al. already does most of this. The
only real current difference is that it creates an encryption key not a
signing key, but that could have an additional bool argument
(signing=true/encryption=false) rather than duplicating the entire
routine set.
Regards,
James
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval
2026-10-08 16:45 ` James Bottomley
@ 2026-10-09 8:29 ` Matthew Garrett
0 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-09 8:29 UTC (permalink / raw)
To: James Bottomley
Cc: Matthew Garrett, keyrings, linux-integrity, rafael, linux-pm,
linux-efi
On Thu, Oct 08, 2026 at 06:45:15PM +0200, James Bottomley wrote:
> Oof, there's a lot of duplication in here. Basically you're creating a
> signing primary key (technically it's not an attestation key because
> they're supposed to be non-primary; this isn't a criticism because the
> only consumer is the machine owner who doesn't need to use privacy
> preserving AKs because they know all about the TPM in their own
> machine). The primary generation routines that exist in tpm2-
> sessions.c:tpm2_create_primary() et al. already does most of this. The
> only real current difference is that it creates an encryption key not a
> signing key, but that could have an additional bool argument
> (signing=true/encryption=false) rather than duplicating the entire
> routine set.
ACK, that's fair. I'll respin with that in mind.
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH 06/17] tpm: Use TPM2_NV_ReadPublic to read NV index names
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (4 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 05/17] tpm: Add a kernel attestation key and signed audit digest retrieval Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 07/17] tpm: Add in-kernel support for reading NV indices Matthew Garrett
` (11 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
tpm2_read_public() is used by tpm_buf_append_name() to find the name of
a handle for HMAC session calculations. It treats NV indices as needing
a lookup, but sends TPM2_ReadPublic, which only accepts objects, so the
lookup fails for any NV index.
Send TPM2_NV_ReadPublic for NV indices instead. Its response has the
same layout of a public area followed by the name, so the existing
parsing works unchanged.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm2-sessions.c | 11 +++++++++--
include/linux/tpm_command.h | 1 +
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/drivers/char/tpm/tpm2-sessions.c b/drivers/char/tpm/tpm2-sessions.c
index badc9bec9caa..e5ef30238fbe 100644
--- a/drivers/char/tpm/tpm2-sessions.c
+++ b/drivers/char/tpm/tpm2-sessions.c
@@ -225,15 +225,22 @@ static int tpm2_read_public(struct tpm_chip *chip, u32 handle, void *name)
if (!buf)
return -ENOMEM;
+ /*
+ * NV indices have their own command, whose response has the same
+ * layout: a public area followed by the name.
+ */
tpm_buf_init(buf, TPM_BUFSIZE);
- tpm_buf_reset(buf, TPM2_ST_NO_SESSIONS, TPM2_CC_READ_PUBLIC);
+ if (mso == TPM2_MSO_NVRAM)
+ tpm_buf_reset(buf, TPM2_ST_NO_SESSIONS, TPM2_CC_NV_READ_PUBLIC);
+ else
+ tpm_buf_reset(buf, TPM2_ST_NO_SESSIONS, TPM2_CC_READ_PUBLIC);
tpm_buf_append_u32(buf, handle);
rc = tpm_transmit_cmd(chip, buf, 0, "TPM2_ReadPublic");
if (rc)
return tpm_ret_to_err(rc);
- /* Skip TPMT_PUBLIC: */
+ /* Skip TPMT_PUBLIC or TPMS_NV_PUBLIC: */
offset += tpm_buf_read_u16(buf, &offset);
/*
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index 486493efa759..9c393f98bac2 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -241,6 +241,7 @@ enum tpm2_command_codes {
TPM2_CC_CONTEXT_LOAD = 0x0161,
TPM2_CC_CONTEXT_SAVE = 0x0162,
TPM2_CC_FLUSH_CONTEXT = 0x0165,
+ TPM2_CC_NV_READ_PUBLIC = 0x0169,
TPM2_CC_READ_PUBLIC = 0x0173,
TPM2_CC_START_AUTH_SESS = 0x0176,
TPM2_CC_VERIFY_SIGNATURE = 0x0177,
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 07/17] tpm: Add in-kernel support for reading NV indices
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (5 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 06/17] tpm: Use TPM2_NV_ReadPublic to read NV index names Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 08/17] tpm: Add NV define, undefine and write helpers Matthew Garrett
` (10 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm_nv_read(), which reads a range of a TPM 2.0 NV index using
TPM2_NV_Read. The caller supplies the authorization handle, which may
be the index itself or a hierarchy depending on the index's
attributes, and which must have an empty auth value.
Reads are split into chunks no larger than the TPM's reported
TPM2_PT_NV_BUFFER_MAX, falling back to the 512 byte minimum required
by the PC Client profile if it cannot be queried. When HMAC sessions
are enabled the reads are integrity protected and the responses
encrypted. If an audit session is already active it is used, so the
reads appear in its log. TPM errors such as a missing index leave the
session intact.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm-interface.c | 36 +++++++++++
drivers/char/tpm/tpm.h | 2 +
drivers/char/tpm/tpm2-cmd.c | 106 +++++++++++++++++++++++++++++++
include/linux/tpm.h | 8 +++
4 files changed, 152 insertions(+)
diff --git a/drivers/char/tpm/tpm-interface.c b/drivers/char/tpm/tpm-interface.c
index 1ccdbde98b69..b5540493dfe6 100644
--- a/drivers/char/tpm/tpm-interface.c
+++ b/drivers/char/tpm/tpm-interface.c
@@ -358,6 +358,42 @@ int tpm_pcr_read(struct tpm_chip *chip, u32 pcr_idx,
}
EXPORT_SYMBOL_GPL(tpm_pcr_read);
+/**
+ * tpm_nv_read - read data from a TPM 2.0 NV index
+ * @chip: a &struct tpm_chip instance
+ * @nv_index: the NV index to read
+ * @auth_handle: the handle authorizing the read: @nv_index itself, or
+ * the owner or platform hierarchy, depending on the index's
+ * attributes. Its auth value must be empty.
+ * @offset: offset within the NV index to start reading at
+ * @data: buffer to receive the data
+ * @len: number of bytes to read
+ *
+ * Return: same as with tpm_transmit_cmd()
+ */
+int tpm_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, u8 *data, u16 len)
+{
+ int rc;
+
+ if (!chip)
+ return -ENODEV;
+
+ rc = tpm_try_get_ops(chip);
+ if (rc)
+ return rc;
+
+ if (chip->flags & TPM_CHIP_FLAG_TPM2)
+ rc = tpm2_nv_read(chip, nv_index, auth_handle, offset, data,
+ len);
+ else
+ rc = -EOPNOTSUPP;
+
+ tpm_put_ops(chip);
+ return rc;
+}
+EXPORT_SYMBOL_GPL(tpm_nv_read);
+
/**
* tpm_pcr_extend - extend a PCR value in SHA1 bank.
* @chip: a &struct tpm_chip instance, %NULL for the default chip
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index 23070bdb2aa4..5bd3b5658909 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -113,6 +113,8 @@ int tpm2_pcr_read(struct tpm_chip *chip, u32 pcr_idx,
int tpm2_pcr_extend(struct tpm_chip *chip, u32 pcr_idx,
struct tpm_digest *digests);
int tpm2_get_random(struct tpm_chip *chip, u8 *dest, size_t max);
+int tpm2_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, u8 *data, u16 len);
ssize_t tpm2_get_tpm_pt(struct tpm_chip *chip, u32 property_id,
u32 *value, const char *desc);
diff --git a/drivers/char/tpm/tpm2-cmd.c b/drivers/char/tpm/tpm2-cmd.c
index bfad86ed0132..5c2831ee3981 100644
--- a/drivers/char/tpm/tpm2-cmd.c
+++ b/drivers/char/tpm/tpm2-cmd.c
@@ -323,6 +323,112 @@ int tpm2_get_random(struct tpm_chip *chip, u8 *dest, size_t max)
return total ? total : -EIO;
}
+/* Minimum MAX_NV_BUFFER_SIZE required by the PC Client TPM profile */
+#define TPM2_NV_BUFFER_MIN 512
+
+/**
+ * tpm2_nv_read() - read data from an NV index
+ * @chip: TPM chip to use
+ * @nv_index: the NV index to read
+ * @auth_handle: the handle authorizing the read, which must have an empty
+ * auth value
+ * @offset: offset within the NV index to start reading at
+ * @data: buffer to receive the data
+ * @len: number of bytes to read
+ *
+ * Reads are split into chunks no larger than the TPM's NV buffer. When
+ * HMAC sessions are enabled the read is integrity protected and the
+ * response encrypted. If an audit session is active it is used, so the
+ * reads are recorded in its log.
+ *
+ * Return: same as with tpm_transmit_cmd()
+ */
+int tpm2_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, u8 *data, u16 len)
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ u32 chunk_max;
+ off_t off;
+ int rc;
+
+ if ((u32)offset + len > U16_MAX + 1)
+ return -EINVAL;
+
+ if (tpm2_get_tpm_pt(chip, TPM2_PT_NV_BUFFER_MAX, &chunk_max, NULL) ||
+ !chunk_max)
+ chunk_max = TPM2_NV_BUFFER_MIN;
+
+ rc = tpm2_start_auth_session(chip, false);
+ /* -EBUSY means an audit session is active, which we can use */
+ if (rc && rc != -EBUSY)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf) {
+ tpm2_end_auth_session(chip);
+ return -ENOMEM;
+ }
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+
+ while (len) {
+ u16 chunk = min_t(u32, len, chunk_max);
+ u16 size;
+
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_NV_READ);
+
+ /* these end the session on failure */
+ rc = tpm_buf_append_name(chip, buf, auth_handle, NULL);
+ if (rc)
+ return rc;
+ rc = tpm_buf_append_name(chip, buf, nv_index, NULL);
+ if (rc)
+ return rc;
+
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_ENCRYPT |
+ TPM2_SA_CONTINUE_SESSION, NULL, 0);
+ tpm_buf_append_u16(buf, chunk);
+ tpm_buf_append_u16(buf, offset);
+
+ rc = tpm_buf_fill_hmac_session(chip, buf);
+ if (rc)
+ return rc;
+
+ rc = tpm_transmit_cmd(chip, buf, 0, "attempting to read NV index");
+ rc = tpm_buf_check_hmac_response(chip, buf, rc);
+ if (rc) {
+ /*
+ * A TPM error such as a missing index leaves the
+ * session usable, so only end it on a system error.
+ */
+ if (rc < 0)
+ tpm2_end_auth_session(chip);
+ return rc;
+ }
+
+ /* skip the parameter size */
+ off = TPM_HEADER_SIZE + sizeof(u32);
+ if (tpm_buf_length(buf) < off + sizeof(u16))
+ goto err_short;
+ size = get_unaligned_be16(&buf->data[off]);
+ off += sizeof(u16);
+ if (size != chunk || tpm_buf_length(buf) < off + size)
+ goto err_short;
+
+ memcpy(data, &buf->data[off], size);
+ data += size;
+ offset += size;
+ len -= size;
+ }
+
+ return 0;
+
+err_short:
+ /* the response passed the HMAC check, so the session is still valid */
+ dev_err(&chip->dev, "short NV read response\n");
+ return -EIO;
+}
+
/**
* tpm2_flush_context() - execute a TPM2_FlushContext command
* @chip: TPM chip to use
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 1d828b32847f..d5a3320efe8b 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -257,6 +257,8 @@ extern int tpm_pcr_read(struct tpm_chip *chip, u32 pcr_idx,
extern int tpm_pcr_extend(struct tpm_chip *chip, u32 pcr_idx,
struct tpm_digest *digests);
extern int tpm_get_random(struct tpm_chip *chip, u8 *data, size_t max);
+extern int tpm_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, u8 *data, u16 len);
extern struct tpm_chip *tpm_default_chip(void);
void tpm2_flush_context(struct tpm_chip *chip, u32 handle);
int tpm2_find_hash_alg(unsigned int crypto_id);
@@ -292,6 +294,12 @@ static inline int tpm_get_random(struct tpm_chip *chip, u8 *data, size_t max)
return -ENODEV;
}
+static inline int tpm_nv_read(struct tpm_chip *chip, u32 nv_index,
+ u32 auth_handle, u16 offset, u8 *data, u16 len)
+{
+ return -ENODEV;
+}
+
static inline struct tpm_chip *tpm_default_chip(void)
{
return NULL;
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 08/17] tpm: Add NV define, undefine and write helpers
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (6 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 07/17] tpm: Add in-kernel support for reading NV indices Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 09/17] tpm: Provision the kernel NV index at registration Matthew Garrett
` (9 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm2_nv_define(), tpm2_nv_undefine() and tpm2_nv_write() alongside
tpm2_nv_read(). Definition and removal use the owner hierarchy, and
indices are defined as ordinary indices with an empty auth value and no
policy. As with reads, the commands use the kernel's HMAC session when
available, with writes encrypting the data, and use an active audit
session if there is one.
Factor the session setup and transmit/check sequence shared with
tpm2_nv_read() into helpers to reduce boilerplate duplication, and add
the TPMA_NV attribute bits needed to define an index.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm.h | 4 +
drivers/char/tpm/tpm2-cmd.c | 229 +++++++++++++++++++++++++++++++++---
include/linux/tpm_command.h | 8 ++
3 files changed, 222 insertions(+), 19 deletions(-)
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index 5bd3b5658909..880327e3faa9 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -115,6 +115,10 @@ int tpm2_pcr_extend(struct tpm_chip *chip, u32 pcr_idx,
int tpm2_get_random(struct tpm_chip *chip, u8 *dest, size_t max);
int tpm2_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
u16 offset, u8 *data, u16 len);
+int tpm2_nv_write(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, const u8 *data, u16 len);
+int tpm2_nv_define(struct tpm_chip *chip, u32 nv_index, u32 attrs, u16 size);
+int tpm2_nv_undefine(struct tpm_chip *chip, u32 nv_index);
ssize_t tpm2_get_tpm_pt(struct tpm_chip *chip, u32 property_id,
u32 *value, const char *desc);
diff --git a/drivers/char/tpm/tpm2-cmd.c b/drivers/char/tpm/tpm2-cmd.c
index 5c2831ee3981..0de7bcd9ec07 100644
--- a/drivers/char/tpm/tpm2-cmd.c
+++ b/drivers/char/tpm/tpm2-cmd.c
@@ -326,6 +326,212 @@ int tpm2_get_random(struct tpm_chip *chip, u8 *dest, size_t max)
/* Minimum MAX_NV_BUFFER_SIZE required by the PC Client TPM profile */
#define TPM2_NV_BUFFER_MIN 512
+static u32 tpm2_nv_buffer_max(struct tpm_chip *chip)
+{
+ u32 val;
+
+ if (tpm2_get_tpm_pt(chip, TPM2_PT_NV_BUFFER_MAX, &val, NULL) || !val)
+ return TPM2_NV_BUFFER_MIN;
+
+ return val;
+}
+
+/*
+ * Start an HMAC session for an NV command. -EBUSY means an audit session
+ * is already active, which is used instead so the command is audited.
+ */
+static int tpm2_nv_session_begin(struct tpm_chip *chip)
+{
+ int rc = tpm2_start_auth_session(chip, false);
+
+ return rc == -EBUSY ? 0 : rc;
+}
+
+/*
+ * Send a command built with HMAC sessions and check the response. A TPM
+ * error such as a missing index leaves the session usable, so it is only
+ * ended on a system error.
+ */
+static int tpm2_nv_transmit(struct tpm_chip *chip, struct tpm_buf *buf,
+ const char *desc)
+{
+ int rc;
+
+ rc = tpm_buf_fill_hmac_session(chip, buf);
+ if (rc)
+ return rc;
+
+ rc = tpm_transmit_cmd(chip, buf, 0, desc);
+ rc = tpm_buf_check_hmac_response(chip, buf, rc);
+ if (rc < 0)
+ tpm2_end_auth_session(chip);
+
+ return rc;
+}
+
+/**
+ * tpm2_nv_define() - define an ordinary NV index in the owner hierarchy
+ * @chip: TPM chip to use
+ * @nv_index: the NV index to define
+ * @attrs: TPMA_NV attributes of the index
+ * @size: size of the index in bytes
+ *
+ * The index is given an empty auth value and no policy. The owner
+ * hierarchy must have an empty auth value.
+ *
+ * Return: same as with tpm_transmit_cmd()
+ */
+int tpm2_nv_define(struct tpm_chip *chip, u32 nv_index, u32 attrs, u16 size)
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ int rc;
+
+ rc = tpm2_nv_session_begin(chip);
+ if (rc)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf) {
+ tpm2_end_auth_session(chip);
+ return -ENOMEM;
+ }
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_NV_DEFINE_SPACE);
+
+ rc = tpm_buf_append_name(chip, buf, TPM2_RH_OWNER, NULL);
+ if (rc)
+ return rc;
+
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION,
+ NULL, 0);
+
+ /* auth (empty) */
+ tpm_buf_append_u16(buf, 0);
+
+ /* publicInfo: TPM2B_NV_PUBLIC */
+ tpm_buf_append_u16(buf, sizeof(u32) + sizeof(u16) + sizeof(u32) +
+ sizeof(u16) + sizeof(u16));
+ tpm_buf_append_u32(buf, nv_index);
+ tpm_buf_append_u16(buf, TPM_ALG_SHA256);
+ tpm_buf_append_u32(buf, attrs);
+ /* authPolicy (empty) */
+ tpm_buf_append_u16(buf, 0);
+ tpm_buf_append_u16(buf, size);
+
+ return tpm2_nv_transmit(chip, buf, "attempting to define NV index");
+}
+
+/**
+ * tpm2_nv_undefine() - undefine an NV index in the owner hierarchy
+ * @chip: TPM chip to use
+ * @nv_index: the NV index to undefine
+ *
+ * The owner hierarchy must have an empty auth value.
+ *
+ * Return: same as with tpm_transmit_cmd()
+ */
+int tpm2_nv_undefine(struct tpm_chip *chip, u32 nv_index)
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ int rc;
+
+ rc = tpm2_nv_session_begin(chip);
+ if (rc)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf) {
+ tpm2_end_auth_session(chip);
+ return -ENOMEM;
+ }
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_NV_UNDEFINE_SPACE);
+
+ rc = tpm_buf_append_name(chip, buf, TPM2_RH_OWNER, NULL);
+ if (rc)
+ return rc;
+ rc = tpm_buf_append_name(chip, buf, nv_index, NULL);
+ if (rc)
+ return rc;
+
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION,
+ NULL, 0);
+
+ return tpm2_nv_transmit(chip, buf, "attempting to undefine NV index");
+}
+
+/**
+ * tpm2_nv_write() - write data to an NV index
+ * @chip: TPM chip to use
+ * @nv_index: the NV index to write
+ * @auth_handle: the handle authorizing the write, which must have an empty
+ * auth value
+ * @offset: offset within the NV index to start writing at
+ * @data: data to write
+ * @len: number of bytes to write
+ *
+ * Writes are split into chunks no larger than the TPM's NV buffer. When
+ * HMAC sessions are enabled the write is integrity protected and the data
+ * encrypted.
+ *
+ * Return: same as with tpm_transmit_cmd()
+ */
+int tpm2_nv_write(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
+ u16 offset, const u8 *data, u16 len)
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ u32 chunk_max;
+ int rc;
+
+ if ((u32)offset + len > U16_MAX + 1)
+ return -EINVAL;
+
+ chunk_max = tpm2_nv_buffer_max(chip);
+
+ rc = tpm2_nv_session_begin(chip);
+ if (rc)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf) {
+ tpm2_end_auth_session(chip);
+ return -ENOMEM;
+ }
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+
+ while (len) {
+ u16 chunk = min_t(u32, len, chunk_max);
+
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_NV_WRITE);
+
+ rc = tpm_buf_append_name(chip, buf, auth_handle, NULL);
+ if (rc)
+ return rc;
+ rc = tpm_buf_append_name(chip, buf, nv_index, NULL);
+ if (rc)
+ return rc;
+
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_DECRYPT |
+ TPM2_SA_CONTINUE_SESSION, NULL, 0);
+ tpm_buf_append_u16(buf, chunk);
+ tpm_buf_append(buf, data, chunk);
+ tpm_buf_append_u16(buf, offset);
+
+ rc = tpm2_nv_transmit(chip, buf, "attempting to write NV index");
+ if (rc)
+ return rc;
+
+ data += chunk;
+ offset += chunk;
+ len -= chunk;
+ }
+
+ return 0;
+}
+
/**
* tpm2_nv_read() - read data from an NV index
* @chip: TPM chip to use
@@ -354,13 +560,10 @@ int tpm2_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
if ((u32)offset + len > U16_MAX + 1)
return -EINVAL;
- if (tpm2_get_tpm_pt(chip, TPM2_PT_NV_BUFFER_MAX, &chunk_max, NULL) ||
- !chunk_max)
- chunk_max = TPM2_NV_BUFFER_MIN;
+ chunk_max = tpm2_nv_buffer_max(chip);
- rc = tpm2_start_auth_session(chip, false);
- /* -EBUSY means an audit session is active, which we can use */
- if (rc && rc != -EBUSY)
+ rc = tpm2_nv_session_begin(chip);
+ if (rc)
return rc;
buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
@@ -390,22 +593,10 @@ int tpm2_nv_read(struct tpm_chip *chip, u32 nv_index, u32 auth_handle,
tpm_buf_append_u16(buf, chunk);
tpm_buf_append_u16(buf, offset);
- rc = tpm_buf_fill_hmac_session(chip, buf);
+ rc = tpm2_nv_transmit(chip, buf, "attempting to read NV index");
if (rc)
return rc;
- rc = tpm_transmit_cmd(chip, buf, 0, "attempting to read NV index");
- rc = tpm_buf_check_hmac_response(chip, buf, rc);
- if (rc) {
- /*
- * A TPM error such as a missing index leaves the
- * session usable, so only end it on a system error.
- */
- if (rc < 0)
- tpm2_end_auth_session(chip);
- return rc;
- }
-
/* skip the parameter size */
off = TPM_HEADER_SIZE + sizeof(u32);
if (tpm_buf_length(buf) < off + sizeof(u16))
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index 9c393f98bac2..c8e867d8bd4b 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -284,6 +284,14 @@ enum tpm2_permanent_handles {
TPM2_RH_ENDORSEMENT = 0x4000000B,
};
+/* TPMA_NV, for an ordinary NV index (TPM_NT_ORDINARY in bits 4-7) */
+enum tpm2_nv_attributes {
+ TPM2_NV_AUTHWRITE = BIT(2),
+ TPM2_NV_AUTHREAD = BIT(18),
+ TPM2_NV_NO_DA = BIT(25),
+ TPM2_NV_WRITTEN = BIT(29),
+};
+
/* TPMS_ATTEST.magic */
#define TPM2_GENERATED_VALUE 0xff544347
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 09/17] tpm: Provision the kernel NV index at registration
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (7 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 08/17] tpm: Add NV define, undefine and write helpers Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 10/17] tpm: Move the bounds-checked response reader to a header Matthew Garrett
` (8 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
The kernel NV index is going to be used to show that the kernel, and
not userspace, was in control of the TPM at a given point in time.
The kernel stores a magic value in it, runs the commands in an audit
session that reads the index, and resets it to zero before releasing
the TPM. Userspace cannot modify the index, so a log showing the magic
value can only have come from the kernel.
Before we expose the TPM to userland, make sure the index exists as an 8
byte ordinary index with an empty auth value, no policy, and the
expected attributes by comparing its name. An index with a different
public area, or one whose auth value turns out not to be empty, is
undefined and defined again to prevent userland or another OS from
leaving the magic value hanging around. The index is then reset to zero
if it holds anything else, which also clears a magic value left behind
by a crash. The owner hierarchy must have an empty auth value.
Add tpm2_kernel_nv_set_magic() and tpm2_kernel_nv_clear() for use by the
kernel while it holds the ops lock. Mark the chip whenever the index may
hold the magic value, and have tpm_dev_transmit() retry the reset and
refuse userspace commands while it is marked. The chip starts out
marked, so that a magic value left behind by a crash is never exposed,
and the mark is only removed once the index is known not to hold the
magic value. This means that if a region has been defined that the kernel
cannot remove (eg, because it was configured to require auth) we can fail
safe.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/Makefile | 1 +
drivers/char/tpm/tpm-chip.c | 30 ++++
drivers/char/tpm/tpm-dev-common.c | 10 ++
drivers/char/tpm/tpm.h | 14 ++
drivers/char/tpm/tpm2-kernel-nv.c | 267 ++++++++++++++++++++++++++++++
include/linux/tpm.h | 3 +
include/linux/tpm_command.h | 1 +
7 files changed, 326 insertions(+)
create mode 100644 drivers/char/tpm/tpm2-kernel-nv.c
diff --git a/drivers/char/tpm/Makefile b/drivers/char/tpm/Makefile
index 10a4ed388dea..88a96fee0934 100644
--- a/drivers/char/tpm/Makefile
+++ b/drivers/char/tpm/Makefile
@@ -18,6 +18,7 @@ tpm-y += eventlog/tpm2.o
tpm-y += tpm-buf.o
tpm-y += tpm2-sessions.o
tpm-$(CONFIG_TCG_TPM2_HMAC) += tpm2-ak.o
+tpm-$(CONFIG_TCG_TPM2_HMAC) += tpm2-kernel-nv.o
tpm-$(CONFIG_ACPI) += tpm_ppi.o eventlog/acpi.o
tpm-$(CONFIG_EFI) += eventlog/efi.o
diff --git a/drivers/char/tpm/tpm-chip.c b/drivers/char/tpm/tpm-chip.c
index cb10f2d1eace..1ec294d081ae 100644
--- a/drivers/char/tpm/tpm-chip.c
+++ b/drivers/char/tpm/tpm-chip.c
@@ -584,6 +584,34 @@ EXPORT_SYMBOL_GPL(tpm_chip_bootstrap);
* This function should be only called after the chip initialization is
* complete.
*/
+/*
+ * Make sure the kernel NV index is defined and does not hold the magic
+ * value before userspace is given access to the TPM.
+ */
+static void tpm_chip_provision_kernel_nv(struct tpm_chip *chip)
+{
+ int rc;
+
+ if (!IS_ENABLED(CONFIG_TCG_TPM2_HMAC) ||
+ !(chip->flags & TPM_CHIP_FLAG_TPM2))
+ return;
+
+ /*
+ * A crash while the magic value was set leaves it in the index, so
+ * assume the worst until provisioning shows otherwise.
+ */
+ chip->flags |= TPM_CHIP_FLAG_KERNEL_NV_DIRTY;
+
+ if (tpm_try_get_ops(chip))
+ return;
+
+ rc = tpm2_kernel_nv_provision(chip);
+ tpm_put_ops(chip);
+
+ if (rc)
+ dev_warn(&chip->dev, "kernel NV index unavailable: %d\n", rc);
+}
+
int tpm_chip_register(struct tpm_chip *chip)
{
int rc;
@@ -602,6 +630,8 @@ int tpm_chip_register(struct tpm_chip *chip)
if (rc)
goto out_ppi;
+ tpm_chip_provision_kernel_nv(chip);
+
rc = tpm_add_char_device(chip);
if (rc)
goto out_hwrng;
diff --git a/drivers/char/tpm/tpm-dev-common.c b/drivers/char/tpm/tpm-dev-common.c
index 1fd93cdaf306..44323f55da51 100644
--- a/drivers/char/tpm/tpm-dev-common.c
+++ b/drivers/char/tpm/tpm-dev-common.c
@@ -94,6 +94,16 @@ static ssize_t tpm_dev_transmit(struct tpm_chip *chip, struct tpm_space *space,
tpm2_dev_cmd_is_blocked(buf, bufsiz))
return -EPERM;
+ /*
+ * Never give userspace access while the kernel NV index may hold
+ * the magic value.
+ */
+ if (chip->flags & TPM_CHIP_FLAG_KERNEL_NV_DIRTY) {
+ tpm2_kernel_nv_clear(chip);
+ if (chip->flags & TPM_CHIP_FLAG_KERNEL_NV_DIRTY)
+ return -EPERM;
+ }
+
if (chip->flags & TPM_CHIP_FLAG_TPM2)
tpm2_end_auth_session(chip);
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index 880327e3faa9..54e8d5820b6d 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -152,11 +152,25 @@ void tpm_dev_common_exit(void);
int tpm2_sessions_init(struct tpm_chip *chip);
void tpm2_free_auth(struct tpm2_auth *auth);
int tpm2_audit_session_handle(struct tpm_chip *chip, u32 *handle);
+
+extern const u8 tpm2_kernel_nv_magic[TPM2_KERNEL_NV_SIZE];
+void tpm2_kernel_nv_name(bool written, u8 name[TPM2_NULL_NAME_SIZE]);
+int tpm2_kernel_nv_provision(struct tpm_chip *chip);
+int tpm2_kernel_nv_set_magic(struct tpm_chip *chip);
+int tpm2_kernel_nv_clear(struct tpm_chip *chip);
#else
static inline int tpm2_sessions_init(struct tpm_chip *chip)
{
return 0;
}
+static inline int tpm2_kernel_nv_provision(struct tpm_chip *chip)
+{
+ return 0;
+}
+static inline int tpm2_kernel_nv_clear(struct tpm_chip *chip)
+{
+ return 0;
+}
#endif
#endif
diff --git a/drivers/char/tpm/tpm2-kernel-nv.c b/drivers/char/tpm/tpm2-kernel-nv.c
new file mode 100644
index 000000000000..cd9ec0f24c2f
--- /dev/null
+++ b/drivers/char/tpm/tpm2-kernel-nv.c
@@ -0,0 +1,267 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Management of the kernel-owned NV index.
+ *
+ * TPM2_KERNEL_NV_INDEX is an 8 byte ordinary NV index with an empty auth
+ * value. Userspace is prevented from modifying it by the /dev/tpm command
+ * filter, so while it holds tpm2_kernel_nv_magic the kernel is the only
+ * party that can have put it there. The kernel sets the magic value only
+ * while it holds the chip's ops lock and resets the index to zero before
+ * releasing it, so userspace never observes the magic value.
+ */
+
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <crypto/sha2.h>
+#include "tpm.h"
+
+#define TPM2_KERNEL_NV_ATTRS \
+ (TPM2_NV_AUTHWRITE | TPM2_NV_AUTHREAD | TPM2_NV_NO_DA)
+
+const u8 tpm2_kernel_nv_magic[TPM2_KERNEL_NV_SIZE] = "LNXKEYGN";
+static const u8 tpm2_kernel_nv_zero[TPM2_KERNEL_NV_SIZE];
+
+/**
+ * tpm2_kernel_nv_name() - compute the expected name of the kernel NV index
+ * @written: whether the index has been written (TPMA_NV_WRITTEN is set)
+ * @name: filled with the name: the SHA256 algorithm ID followed by the
+ * hash of the index's TPMS_NV_PUBLIC
+ */
+void tpm2_kernel_nv_name(bool written, u8 name[TPM2_NULL_NAME_SIZE])
+{
+ u32 attrs = TPM2_KERNEL_NV_ATTRS | (written ? TPM2_NV_WRITTEN : 0);
+ u8 pub[14];
+
+ /* TPMS_NV_PUBLIC */
+ put_unaligned_be32(TPM2_KERNEL_NV_INDEX, &pub[0]);
+ put_unaligned_be16(TPM_ALG_SHA256, &pub[4]);
+ put_unaligned_be32(attrs, &pub[6]);
+ /* authPolicy (empty) */
+ put_unaligned_be16(0, &pub[10]);
+ put_unaligned_be16(TPM2_KERNEL_NV_SIZE, &pub[12]);
+
+ put_unaligned_be16(TPM_ALG_SHA256, name);
+ sha256(pub, sizeof(pub), name + 2);
+}
+
+/*
+ * Read the name of the kernel NV index. Returns 0 and fills @name, a
+ * positive TPM error code if the index does not exist, or -errno.
+ */
+static int tpm2_kernel_nv_read_name(struct tpm_chip *chip,
+ u8 name[TPM2_NULL_NAME_SIZE])
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ off_t offset = TPM_HEADER_SIZE;
+ u16 len;
+ int rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_NO_SESSIONS, TPM2_CC_NV_READ_PUBLIC);
+ tpm_buf_append_u32(buf, TPM2_KERNEL_NV_INDEX);
+
+ rc = tpm_transmit_cmd(chip, buf, 0, NULL);
+ if (rc)
+ return rc;
+
+ /* skip nvPublic */
+ if (tpm_buf_length(buf) < offset + sizeof(u16))
+ return -EIO;
+ offset += sizeof(u16) + get_unaligned_be16(&buf->data[offset]);
+
+ /* nvName */
+ if (tpm_buf_length(buf) < offset + sizeof(u16))
+ return -EIO;
+ len = get_unaligned_be16(&buf->data[offset]);
+ offset += sizeof(u16);
+ if (len != TPM2_NULL_NAME_SIZE || tpm_buf_length(buf) < offset + len)
+ return -EIO;
+
+ memcpy(name, &buf->data[offset], len);
+ return 0;
+}
+
+static int tpm2_kernel_nv_write(struct tpm_chip *chip, const u8 *value)
+{
+ return tpm2_nv_write(chip, TPM2_KERNEL_NV_INDEX, TPM2_KERNEL_NV_INDEX,
+ 0, value, TPM2_KERNEL_NV_SIZE);
+}
+
+/*
+ * Determine whether the index may hold the magic value, without changing
+ * it. Only a definite answer that it does not is trusted: the index does
+ * not exist, has never been written, or holds some other value.
+ */
+static bool tpm2_kernel_nv_may_hold_magic(struct tpm_chip *chip)
+{
+ u8 name[TPM2_NULL_NAME_SIZE], value[TPM2_KERNEL_NV_SIZE];
+ int rc;
+
+ rc = tpm2_kernel_nv_read_name(chip, name);
+ if (rc > 0 && tpm2_rc_value(rc) == TPM2_RC_HANDLE)
+ return false;
+ if (rc)
+ return true;
+
+ rc = tpm2_nv_read(chip, TPM2_KERNEL_NV_INDEX, TPM2_KERNEL_NV_INDEX, 0,
+ value, sizeof(value));
+ if (rc == TPM2_RC_NV_UNINITIALIZED)
+ return false;
+ if (rc)
+ return true;
+
+ return !memcmp(value, tpm2_kernel_nv_magic, sizeof(value));
+}
+
+static void tpm2_kernel_nv_update_dirty(struct tpm_chip *chip, bool dirty)
+{
+ if (dirty)
+ chip->flags |= TPM_CHIP_FLAG_KERNEL_NV_DIRTY;
+ else
+ chip->flags &= ~TPM_CHIP_FLAG_KERNEL_NV_DIRTY;
+}
+
+static int tpm2_kernel_nv_recreate(struct tpm_chip *chip)
+{
+ int rc;
+
+ rc = tpm2_nv_undefine(chip, TPM2_KERNEL_NV_INDEX);
+ if (rc)
+ return rc;
+
+ rc = tpm2_nv_define(chip, TPM2_KERNEL_NV_INDEX, TPM2_KERNEL_NV_ATTRS,
+ TPM2_KERNEL_NV_SIZE);
+ if (rc)
+ return rc;
+
+ return tpm2_kernel_nv_write(chip, tpm2_kernel_nv_zero);
+}
+
+/*
+ * Reset the index to zero unless it already is, to avoid an NV write on
+ * every boot.
+ */
+static int tpm2_kernel_nv_ensure_zero(struct tpm_chip *chip)
+{
+ u8 value[TPM2_KERNEL_NV_SIZE];
+ int rc;
+
+ rc = tpm2_nv_read(chip, TPM2_KERNEL_NV_INDEX, TPM2_KERNEL_NV_INDEX, 0,
+ value, sizeof(value));
+ if (!rc && !memcmp(value, tpm2_kernel_nv_zero, sizeof(value)))
+ return 0;
+
+ return tpm2_kernel_nv_clear(chip);
+}
+
+static int __tpm2_kernel_nv_provision(struct tpm_chip *chip)
+{
+ u8 expected[TPM2_NULL_NAME_SIZE], name[TPM2_NULL_NAME_SIZE];
+ int rc;
+
+ rc = tpm2_kernel_nv_read_name(chip, name);
+ if (rc < 0)
+ return rc;
+
+ if (rc > 0) {
+ /* the index does not exist */
+ rc = tpm2_nv_define(chip, TPM2_KERNEL_NV_INDEX,
+ TPM2_KERNEL_NV_ATTRS, TPM2_KERNEL_NV_SIZE);
+ if (rc)
+ return rc;
+
+ return tpm2_kernel_nv_clear(chip);
+ }
+
+ tpm2_kernel_nv_name(true, expected);
+ if (memcmp(name, expected, sizeof(name))) {
+ tpm2_kernel_nv_name(false, expected);
+ if (memcmp(name, expected, sizeof(name))) {
+ dev_warn(&chip->dev,
+ "kernel NV index has unexpected attributes, redefining\n");
+ rc = tpm2_kernel_nv_recreate(chip);
+ if (rc)
+ return rc;
+
+ return tpm2_kernel_nv_clear(chip);
+ }
+ }
+
+ rc = tpm2_kernel_nv_ensure_zero(chip);
+ if (rc > 0) {
+ /* auth value is not empty: the index was not defined by us */
+ dev_warn(&chip->dev,
+ "kernel NV index cannot be written, redefining\n");
+ rc = tpm2_kernel_nv_recreate(chip);
+ if (rc)
+ return rc;
+
+ return tpm2_kernel_nv_clear(chip);
+ }
+
+ return rc;
+}
+
+/**
+ * tpm2_kernel_nv_provision() - ensure the kernel NV index is usable
+ * @chip: the TPM chip
+ *
+ * Makes sure the kernel NV index exists with the expected public area and
+ * an empty auth value, and that it holds zero. An index with an unexpected
+ * public area, or one that cannot be written with an empty auth value, is
+ * undefined and defined again. The owner hierarchy must have an empty auth
+ * value. If this fails and the index may still hold the magic value, the
+ * chip is marked so that userspace commands are refused. The caller must
+ * hold the chip's ops lock.
+ *
+ * Return: 0 on success, -errno or a TPM error code on failure.
+ */
+int tpm2_kernel_nv_provision(struct tpm_chip *chip)
+{
+ int rc = __tpm2_kernel_nv_provision(chip);
+
+ tpm2_kernel_nv_update_dirty(chip,
+ rc && tpm2_kernel_nv_may_hold_magic(chip));
+ return rc;
+}
+
+/**
+ * tpm2_kernel_nv_set_magic() - store the magic value in the kernel NV index
+ * @chip: the TPM chip
+ *
+ * The caller must hold the chip's ops lock, and must call
+ * tpm2_kernel_nv_clear() before releasing it.
+ *
+ * Return: 0 on success, -errno or a TPM error code on failure.
+ */
+int tpm2_kernel_nv_set_magic(struct tpm_chip *chip)
+{
+ /* assume the worst until the index is known to be clear again */
+ chip->flags |= TPM_CHIP_FLAG_KERNEL_NV_DIRTY;
+
+ return tpm2_kernel_nv_write(chip, tpm2_kernel_nv_magic);
+}
+
+/**
+ * tpm2_kernel_nv_clear() - reset the kernel NV index to zero
+ * @chip: the TPM chip
+ *
+ * If the index cannot be reset, it may still hold the magic value, so the
+ * chip is marked so that userspace commands are refused until a reset
+ * succeeds. The caller must hold the chip's ops lock.
+ *
+ * Return: 0 on success, -errno or a TPM error code on failure.
+ */
+int tpm2_kernel_nv_clear(struct tpm_chip *chip)
+{
+ int rc;
+
+ rc = tpm2_kernel_nv_write(chip, tpm2_kernel_nv_zero);
+ tpm2_kernel_nv_update_dirty(chip,
+ rc && tpm2_kernel_nv_may_hold_magic(chip));
+ return rc;
+}
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index d5a3320efe8b..35ba30a2674d 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -183,6 +183,7 @@ static inline enum tpm2_mso_type tpm2_handle_mso(u32 handle)
* undefine or modify it.
*/
#define TPM2_KERNEL_NV_INDEX 0x014c4853
+#define TPM2_KERNEL_NV_SIZE 8
#define TPM_VID_INTEL 0x8086
#define TPM_VID_WINBOND 0x1050
@@ -203,6 +204,8 @@ enum tpm_chip_flags {
TPM_CHIP_FLAG_HWRNG_DISABLED = BIT(9),
TPM_CHIP_FLAG_DISABLE = BIT(10),
TPM_CHIP_FLAG_SYNC = BIT(11),
+ /* the kernel NV index may hold the magic value */
+ TPM_CHIP_FLAG_KERNEL_NV_DIRTY = BIT(12),
};
#define to_tpm_chip(d) container_of(d, struct tpm_chip, dev)
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index c8e867d8bd4b..cef85f532b6c 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -203,6 +203,7 @@ enum tpm2_return_codes {
TPM2_RC_HANDLE = 0x008B,
TPM2_RC_INTEGRITY = 0x009F,
TPM2_RC_INITIALIZE = 0x0100, /* RC_VER1 */
+ TPM2_RC_NV_UNINITIALIZED = 0x014A,
TPM2_RC_FAILURE = 0x0101,
TPM2_RC_DISABLED = 0x0120,
TPM2_RC_UPGRADE = 0x012D,
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 10/17] tpm: Move the bounds-checked response reader to a header
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (8 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 09/17] tpm: Provision the kernel NV index at registration Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance Matthew Garrett
` (7 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
"tpm: Add NV define, undefine and write helpers" added some helper code
to reduce duplication in parsing TPM responses. This could be useful
elsewhere, so move it out to a header and add some additional features
that will be used shortly.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm2-ak.c | 89 +-----------------------
drivers/char/tpm/tpm2-rsp.h | 132 ++++++++++++++++++++++++++++++++++++
2 files changed, 134 insertions(+), 87 deletions(-)
create mode 100644 drivers/char/tpm/tpm2-rsp.h
diff --git a/drivers/char/tpm/tpm2-ak.c b/drivers/char/tpm/tpm2-ak.c
index ad24d36b1728..18968c8d2b48 100644
--- a/drivers/char/tpm/tpm2-ak.c
+++ b/drivers/char/tpm/tpm2-ak.c
@@ -15,6 +15,7 @@
#include <linux/slab.h>
#include <linux/unaligned.h>
#include "tpm.h"
+#include "tpm2-rsp.h"
/*
* Restricted signing key whose private part never leaves the TPM. NO_DA
@@ -36,96 +37,10 @@
static const u8 tpm2_kernel_ak_seed[EC_PT_SZ] =
"Linux kernel attestation key v1";
-/* Bounds-checked reader for TPM response data */
-struct tpm2_rsp {
- const u8 *data;
- u32 len;
- u32 off;
- bool err;
-};
-
-static const u8 *tpm2_rsp_bytes(struct tpm2_rsp *r, u32 count)
-{
- const u8 *p;
-
- if (r->err || r->len - r->off < count) {
- r->err = true;
- return NULL;
- }
-
- p = &r->data[r->off];
- r->off += count;
- return p;
-}
-
-static u16 tpm2_rsp_u16(struct tpm2_rsp *r)
-{
- const u8 *p = tpm2_rsp_bytes(r, sizeof(u16));
-
- return p ? get_unaligned_be16(p) : 0;
-}
-
-static u32 tpm2_rsp_u32(struct tpm2_rsp *r)
-{
- const u8 *p = tpm2_rsp_bytes(r, sizeof(u32));
-
- return p ? get_unaligned_be32(p) : 0;
-}
-
-/* Initialise a reader over the response held in @buf */
-static void tpm2_rsp_init(struct tpm2_rsp *r, struct tpm_buf *buf)
-{
- struct tpm_header *head = (struct tpm_header *)buf->data;
-
- r->data = buf->data;
- r->len = min_t(u32, be32_to_cpu(head->length), TPM_BUFSIZE);
- r->off = TPM_HEADER_SIZE;
- r->err = r->len < TPM_HEADER_SIZE;
-}
-
-/* Read a TPM2B_ECC_PARAMETER, left-padding it to EC_PT_SZ bytes */
-static void tpm2_rsp_ecc_param(struct tpm2_rsp *r, u8 *out)
-{
- u16 len = tpm2_rsp_u16(r);
- const u8 *p;
-
- if (len > EC_PT_SZ) {
- r->err = true;
- return;
- }
-
- p = tpm2_rsp_bytes(r, len);
- if (!p)
- return;
-
- memset(out, 0, EC_PT_SZ - len);
- memcpy(out + EC_PT_SZ - len, p, len);
-}
-
/* Parse and validate the TPM2B_PUBLIC of the kernel AK */
static int tpm2_parse_kernel_ak_public(struct tpm2_rsp *r, u8 *x, u8 *y)
{
- u16 size = tpm2_rsp_u16(r);
- u32 end = r->off + size;
-
- if (tpm2_rsp_u16(r) != TPM_ALG_ECC ||
- tpm2_rsp_u16(r) != TPM_ALG_SHA256 ||
- tpm2_rsp_u32(r) != TPM2_OA_KERNEL_AK ||
- tpm2_rsp_u16(r) != 0 || /* authPolicy */
- tpm2_rsp_u16(r) != TPM_ALG_NULL || /* symmetric */
- tpm2_rsp_u16(r) != TPM_ALG_ECDSA || /* scheme */
- tpm2_rsp_u16(r) != TPM_ALG_SHA256 || /* scheme hash */
- tpm2_rsp_u16(r) != TPM2_ECC_NIST_P256 ||
- tpm2_rsp_u16(r) != TPM_ALG_NULL) /* kdf */
- return -EINVAL;
-
- tpm2_rsp_ecc_param(r, x);
- tpm2_rsp_ecc_param(r, y);
-
- if (r->err || r->off != end)
- return -EINVAL;
-
- return 0;
+ return tpm2_rsp_ecdsa_public(r, TPM2_OA_KERNEL_AK, x, y);
}
/**
diff --git a/drivers/char/tpm/tpm2-rsp.h b/drivers/char/tpm/tpm2-rsp.h
new file mode 100644
index 000000000000..3ee1f0f2e6ad
--- /dev/null
+++ b/drivers/char/tpm/tpm2-rsp.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Bounds-checked reader for data returned by the TPM. Reading past the
+ * end of the data sets an error flag and returns zeros, so a sequence of
+ * reads can be checked once at the end.
+ */
+#ifndef __TPM2_RSP_H__
+#define __TPM2_RSP_H__
+
+#include <linux/tpm.h>
+#include <linux/unaligned.h>
+
+struct tpm2_rsp {
+ const u8 *data;
+ u32 len;
+ u32 off;
+ bool err;
+};
+
+static inline void tpm2_rsp_init_data(struct tpm2_rsp *r, const u8 *data,
+ u32 len)
+{
+ r->data = data;
+ r->len = len;
+ r->off = 0;
+ r->err = false;
+}
+
+/* Initialise a reader over the parameters of the response held in @buf */
+static inline void tpm2_rsp_init(struct tpm2_rsp *r, struct tpm_buf *buf)
+{
+ struct tpm_header *head = (struct tpm_header *)buf->data;
+
+ r->data = buf->data;
+ r->len = min_t(u32, be32_to_cpu(head->length), TPM_BUFSIZE);
+ r->off = TPM_HEADER_SIZE;
+ r->err = r->len < TPM_HEADER_SIZE;
+}
+
+static inline const u8 *tpm2_rsp_bytes(struct tpm2_rsp *r, u32 count)
+{
+ const u8 *p;
+
+ if (r->err || r->len - r->off < count) {
+ r->err = true;
+ return NULL;
+ }
+
+ p = &r->data[r->off];
+ r->off += count;
+ return p;
+}
+
+static inline u8 tpm2_rsp_u8(struct tpm2_rsp *r)
+{
+ const u8 *p = tpm2_rsp_bytes(r, sizeof(u8));
+
+ return p ? *p : 0;
+}
+
+static inline u16 tpm2_rsp_u16(struct tpm2_rsp *r)
+{
+ const u8 *p = tpm2_rsp_bytes(r, sizeof(u16));
+
+ return p ? get_unaligned_be16(p) : 0;
+}
+
+static inline u32 tpm2_rsp_u32(struct tpm2_rsp *r)
+{
+ const u8 *p = tpm2_rsp_bytes(r, sizeof(u32));
+
+ return p ? get_unaligned_be32(p) : 0;
+}
+
+/* Read a TPM2B, returning its contents and setting @len */
+static inline const u8 *tpm2_rsp_tpm2b(struct tpm2_rsp *r, u16 *len)
+{
+ *len = tpm2_rsp_u16(r);
+ return tpm2_rsp_bytes(r, *len);
+}
+
+/* Read a TPM2B_ECC_PARAMETER, left-padding it to EC_PT_SZ bytes */
+static inline void tpm2_rsp_ecc_param(struct tpm2_rsp *r, u8 *out)
+{
+ u16 len = tpm2_rsp_u16(r);
+ const u8 *p;
+
+ if (len > EC_PT_SZ) {
+ r->err = true;
+ return;
+ }
+
+ p = tpm2_rsp_bytes(r, len);
+ if (!p)
+ return;
+
+ memset(out, 0, EC_PT_SZ - len);
+ memcpy(out + EC_PT_SZ - len, p, len);
+}
+
+/*
+ * Parse a TPM2B_PUBLIC for an ECDSA-SHA256 P-256 signing key with no
+ * symmetric algorithm, KDF or policy, checking that it has exactly the
+ * object attributes @attrs, and return its public point.
+ */
+static inline int tpm2_rsp_ecdsa_public(struct tpm2_rsp *r, u32 attrs,
+ u8 *x, u8 *y)
+{
+ u16 size = tpm2_rsp_u16(r);
+ u32 end = r->off + size;
+
+ if (tpm2_rsp_u16(r) != TPM_ALG_ECC ||
+ tpm2_rsp_u16(r) != TPM_ALG_SHA256 ||
+ tpm2_rsp_u32(r) != attrs ||
+ tpm2_rsp_u16(r) != 0 || /* authPolicy */
+ tpm2_rsp_u16(r) != TPM_ALG_NULL || /* symmetric */
+ tpm2_rsp_u16(r) != TPM_ALG_ECDSA || /* scheme */
+ tpm2_rsp_u16(r) != TPM_ALG_SHA256 || /* scheme hash */
+ tpm2_rsp_u16(r) != TPM2_ECC_NIST_P256 ||
+ tpm2_rsp_u16(r) != TPM_ALG_NULL) /* kdf */
+ return -EINVAL;
+
+ tpm2_rsp_ecc_param(r, x);
+ tpm2_rsp_ecc_param(r, y);
+
+ if (r->err || r->off != end)
+ return -EINVAL;
+
+ return 0;
+}
+
+#endif
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (9 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 10/17] tpm: Move the bounds-checked response reader to a header Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 17:00 ` James Bottomley
2026-10-08 13:20 ` [PATCH 12/17] tpm: Add signing with the kernel signing key Matthew Garrett
` (6 subsequent siblings)
17 siblings, 1 reply; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm2_kernel_key_create(), which creates an ECDSA P-256 signing key
for the kernel along with evidence that the kernel, rather than
userspace, created it. The key is created with TPM2_CreateLoaded under
the null hierarchy primary, with a random auth value that is sent
encrypted and never leaves the kernel. Its private part comes from the
TPM's RNG and the null seed changes on every TPM reset, so it can
neither be recreated nor used after a reboot.
The key is created inside an audit session while the kernel NV index
holds its magic value. Inside that audit session we:
* Read PCR 5. This will be used to bind the key to a kernel that
implements the restricted NV index feature.
* Read the kernel NV index. This will prove that the session was owned
by the kernel - userland will never be able to set this index to the
magic value.
* Generate the key. The above information proves (1) that the kernel
blocks userland from modifying the NV index, and (2) that the NV index
indicated that the session was in-kernel, which means that the key must
have been generated by the kernel.
Once this is done the NV index is set back to 0. The audit log can then
be later used to validate the key identity.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/Kconfig | 10 +
drivers/char/tpm/Makefile | 1 +
drivers/char/tpm/tpm.h | 1 +
drivers/char/tpm/tpm2-kernel-key.c | 404 +++++++++++++++++++++++++++++
drivers/char/tpm/tpm2-sessions.c | 2 +-
include/linux/tpm.h | 39 +++
6 files changed, 456 insertions(+), 1 deletion(-)
create mode 100644 drivers/char/tpm/tpm2-kernel-key.c
diff --git a/drivers/char/tpm/Kconfig b/drivers/char/tpm/Kconfig
index a454b63edca5..15d204f8fa6d 100644
--- a/drivers/char/tpm/Kconfig
+++ b/drivers/char/tpm/Kconfig
@@ -42,6 +42,16 @@ config TCG_TPM2_HMAC
here adds some encryption overhead to all kernel to TPM
transactions.
+config TCG_TPM2_KERNEL_KEY
+ bool "Kernel-generated TPM signing keys with provenance"
+ depends on TCG_TPM2_HMAC
+ help
+ Allow the kernel to create TPM signing keys along with evidence,
+ signed by the kernel attestation key, that the key was created
+ by the kernel rather than by userspace. This is used to sign
+ data that must later be shown to have been produced by the
+ kernel, such as hibernation images.
+
config HW_RANDOM_TPM
bool "TPM HW Random Number Generator support"
depends on TCG_TPM && HW_RANDOM && !(TCG_TPM=y && HW_RANDOM=m)
diff --git a/drivers/char/tpm/Makefile b/drivers/char/tpm/Makefile
index 88a96fee0934..66e5b27ad7c6 100644
--- a/drivers/char/tpm/Makefile
+++ b/drivers/char/tpm/Makefile
@@ -19,6 +19,7 @@ tpm-y += tpm-buf.o
tpm-y += tpm2-sessions.o
tpm-$(CONFIG_TCG_TPM2_HMAC) += tpm2-ak.o
tpm-$(CONFIG_TCG_TPM2_HMAC) += tpm2-kernel-nv.o
+tpm-$(CONFIG_TCG_TPM2_KERNEL_KEY) += tpm2-kernel-key.o
tpm-$(CONFIG_ACPI) += tpm_ppi.o eventlog/acpi.o
tpm-$(CONFIG_EFI) += eventlog/efi.o
diff --git a/drivers/char/tpm/tpm.h b/drivers/char/tpm/tpm.h
index 54e8d5820b6d..228230b2d5bb 100644
--- a/drivers/char/tpm/tpm.h
+++ b/drivers/char/tpm/tpm.h
@@ -152,6 +152,7 @@ void tpm_dev_common_exit(void);
int tpm2_sessions_init(struct tpm_chip *chip);
void tpm2_free_auth(struct tpm2_auth *auth);
int tpm2_audit_session_handle(struct tpm_chip *chip, u32 *handle);
+int tpm2_load_null(struct tpm_chip *chip, u32 *null_key);
extern const u8 tpm2_kernel_nv_magic[TPM2_KERNEL_NV_SIZE];
void tpm2_kernel_nv_name(bool written, u8 name[TPM2_NULL_NAME_SIZE]);
diff --git a/drivers/char/tpm/tpm2-kernel-key.c b/drivers/char/tpm/tpm2-kernel-key.c
new file mode 100644
index 000000000000..27815525bc3f
--- /dev/null
+++ b/drivers/char/tpm/tpm2-kernel-key.c
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Kernel-generated TPM signing keys with provenance.
+ *
+ * A kernel signing key is an ECDSA P-256 key created by the TPM under the
+ * null hierarchy primary, with a random auth value known only to the
+ * kernel. Its private part is generated by the TPM's RNG and cannot be
+ * recreated, and the null seed changes on every TPM reset, so the key
+ * cannot be used once the system has rebooted.
+ *
+ * To show that the key was created by the kernel and not by userspace,
+ * it is created inside an audit session while the kernel NV index holds
+ * its magic value, which userspace is unable to store there:
+ *
+ * set kernel NV index to magic
+ * start audit session
+ * TPM2_PCR_Read(PCR 5, SHA-1 and SHA-256 banks)
+ * TPM2_NV_Read(kernel NV index)
+ * TPM2_CreateLoaded(signing key)
+ * TPM2_GetSessionAuditDigest, signed by the kernel AK
+ * end audit session
+ * reset kernel NV index to zero
+ *
+ * The signed audit digest, the audit log and the parameters of each
+ * response form the key's provenance. A verifier can recompute the
+ * audit digest from the log, check that each logged response matches the
+ * recorded parameters, and so learn that the NV read returned the magic
+ * value, what PCR 5 held, and the public key that was created.
+ */
+
+#include <linux/random.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include "tpm.h"
+#include "tpm2-rsp.h"
+
+#define TPM2_KKEY_PROV_MAGIC 0x544b5056 /* "TKPV" */
+#define TPM2_KKEY_PROV_VERSION 1
+#define TPM2_KKEY_NR_CMDS 3
+#define TPM2_KKEY_RSP_MAX 512
+
+/* Kernel signing keys may sign arbitrary digests */
+#define TPM2_OA_KERNEL_KEY ( \
+ TPM2_OA_FIXED_TPM | \
+ TPM2_OA_FIXED_PARENT | \
+ TPM2_OA_SENSITIVE_DATA_ORIGIN | \
+ TPM2_OA_USER_WITH_AUTH | \
+ TPM2_OA_NO_DA | \
+ TPM2_OA_SIGN)
+
+/* TPML_PCR_SELECTION for PCR 5 in the SHA-1 and SHA-256 banks */
+static const u8 tpm2_kkey_pcr5_select[] = {
+ 0x00, 0x00, 0x00, 0x02, /* count */
+ 0x00, 0x04, /* TPM_ALG_SHA1 */
+ 0x03, /* sizeofSelect */
+ 0x20, 0x00, 0x00, /* PCR 5 */
+ 0x00, 0x0b, /* TPM_ALG_SHA256 */
+ 0x03, /* sizeofSelect */
+ 0x20, 0x00, 0x00, /* PCR 5 */
+};
+
+/* The parameters of a response, as covered by the logged rpHash */
+struct tpm2_kkey_rsp {
+ u32 cc;
+ u16 len;
+ u8 data[TPM2_KKEY_RSP_MAX];
+};
+
+/*
+ * Send a command in the audit session and save its response parameters.
+ * If the response has a handle it is returned in @handle.
+ */
+static int tpm2_kkey_transmit(struct tpm_chip *chip, struct tpm_buf *buf,
+ u32 *handle, struct tpm2_kkey_rsp *rsp,
+ const char *desc)
+{
+ struct tpm_header *head = (struct tpm_header *)buf->data;
+ struct tpm2_rsp r;
+ const u8 *params;
+ u32 len;
+ int rc;
+
+ rsp->cc = be32_to_cpu(head->ordinal);
+
+ rc = tpm_buf_fill_hmac_session(chip, buf);
+ if (rc)
+ return rc;
+
+ rc = tpm_transmit_cmd(chip, buf, 0, desc);
+ rc = tpm_buf_check_hmac_response(chip, buf, rc);
+ if (rc)
+ return rc;
+
+ tpm2_rsp_init(&r, buf);
+ if (handle)
+ *handle = tpm2_rsp_u32(&r);
+ len = tpm2_rsp_u32(&r);
+ params = tpm2_rsp_bytes(&r, len);
+ if (!params || len > sizeof(rsp->data))
+ return -EIO;
+
+ memcpy(rsp->data, params, len);
+ rsp->len = len;
+ return 0;
+}
+
+static int tpm2_kkey_pcr_read(struct tpm_chip *chip, struct tpm_buf *buf,
+ struct tpm2_kkey_rsp *rsp)
+{
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_PCR_READ);
+ /* the session is used only for audit */
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION,
+ NULL, 0);
+ tpm_buf_append(buf, tpm2_kkey_pcr5_select,
+ sizeof(tpm2_kkey_pcr5_select));
+
+ return tpm2_kkey_transmit(chip, buf, NULL, rsp, "reading PCR 5");
+}
+
+static int tpm2_kkey_nv_read(struct tpm_chip *chip, struct tpm_buf *buf,
+ struct tpm2_kkey_rsp *rsp)
+{
+ int rc;
+
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_NV_READ);
+ rc = tpm_buf_append_name(chip, buf, TPM2_KERNEL_NV_INDEX, NULL);
+ if (rc)
+ return rc;
+ rc = tpm_buf_append_name(chip, buf, TPM2_KERNEL_NV_INDEX, NULL);
+ if (rc)
+ return rc;
+
+ /* no response encryption, so the logged rpHash covers the value */
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION,
+ NULL, 0);
+ tpm_buf_append_u16(buf, TPM2_KERNEL_NV_SIZE);
+ tpm_buf_append_u16(buf, 0);
+
+ rc = tpm2_kkey_transmit(chip, buf, NULL, rsp,
+ "reading kernel NV index");
+ if (rc)
+ return rc;
+
+ /* the TPM2B_MAX_NV_BUFFER must hold the magic value */
+ if (rsp->len != sizeof(u16) + TPM2_KERNEL_NV_SIZE ||
+ get_unaligned_be16(rsp->data) != TPM2_KERNEL_NV_SIZE ||
+ memcmp(rsp->data + sizeof(u16), tpm2_kernel_nv_magic,
+ TPM2_KERNEL_NV_SIZE))
+ return -EIO;
+
+ return 0;
+}
+
+static int tpm2_kkey_create_loaded(struct tpm_chip *chip,
+ struct tpm_buf *buf, u32 parent,
+ struct tpm2_kernel_key *key,
+ struct tpm2_kkey_rsp *rsp)
+{
+ struct tpm_buf *template __free(kfree) = NULL;
+ struct tpm2_rsp r;
+ u16 len;
+ int rc;
+
+ template = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!template)
+ return -ENOMEM;
+
+ tpm_buf_init_sized(template, TPM_BUFSIZE);
+ tpm_buf_append_u16(template, TPM_ALG_ECC);
+ tpm_buf_append_u16(template, TPM_ALG_SHA256);
+ tpm_buf_append_u32(template, TPM2_OA_KERNEL_KEY);
+ /* auth policy (empty) */
+ tpm_buf_append_u16(template, 0);
+ /* symmetric algorithm (none for a signing key) */
+ tpm_buf_append_u16(template, TPM_ALG_NULL);
+ /* signing scheme */
+ tpm_buf_append_u16(template, TPM_ALG_ECDSA);
+ tpm_buf_append_u16(template, TPM_ALG_SHA256);
+ tpm_buf_append_u16(template, TPM2_ECC_NIST_P256);
+ /* KDF scheme */
+ tpm_buf_append_u16(template, TPM_ALG_NULL);
+ /* unique (empty points) */
+ tpm_buf_append_u16(template, 0);
+ tpm_buf_append_u16(template, 0);
+
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_CREATE_LOADED);
+ rc = tpm_buf_append_name(chip, buf, parent, chip->null_key_name);
+ if (rc)
+ return rc;
+
+ /* encrypt inSensitive so the key's auth value is not exposed */
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_DECRYPT |
+ TPM2_SA_CONTINUE_SESSION, NULL, 0);
+
+ /* inSensitive: the auth value and no data */
+ tpm_buf_append_u16(buf, sizeof(u16) + sizeof(key->auth) + sizeof(u16));
+ tpm_buf_append_u16(buf, sizeof(key->auth));
+ tpm_buf_append(buf, key->auth, sizeof(key->auth));
+ tpm_buf_append_u16(buf, 0);
+
+ /* inPublic */
+ tpm_buf_append(buf, template->data, template->length);
+
+ if (buf->flags & TPM_BUF_INVALID || template->flags & TPM_BUF_INVALID)
+ return -EINVAL;
+
+ rc = tpm2_kkey_transmit(chip, buf, &key->handle, rsp,
+ "creating kernel signing key");
+ if (rc)
+ return rc;
+
+ /* outPrivate, then outPublic */
+ tpm2_rsp_init_data(&r, rsp->data, rsp->len);
+ tpm2_rsp_tpm2b(&r, &len);
+
+ return tpm2_rsp_ecdsa_public(&r, TPM2_OA_KERNEL_KEY, key->x, key->y);
+}
+
+/* Bounds-checked writer for the serialized provenance */
+struct tpm2_kkey_writer {
+ struct tpm2_key_provenance *prov;
+ bool err;
+};
+
+static void tpm2_kkey_put(struct tpm2_kkey_writer *w, const void *data,
+ u16 len)
+{
+ struct tpm2_key_provenance *prov = w->prov;
+
+ if (w->err || sizeof(prov->data) - prov->len < len) {
+ w->err = true;
+ return;
+ }
+
+ memcpy(&prov->data[prov->len], data, len);
+ prov->len += len;
+}
+
+static void tpm2_kkey_put_u8(struct tpm2_kkey_writer *w, u8 val)
+{
+ tpm2_kkey_put(w, &val, sizeof(val));
+}
+
+static void tpm2_kkey_put_u16(struct tpm2_kkey_writer *w, u16 val)
+{
+ __be16 v = cpu_to_be16(val);
+
+ tpm2_kkey_put(w, &v, sizeof(v));
+}
+
+static void tpm2_kkey_put_u32(struct tpm2_kkey_writer *w, u32 val)
+{
+ __be32 v = cpu_to_be32(val);
+
+ tpm2_kkey_put(w, &v, sizeof(v));
+}
+
+/*
+ * Serialized provenance, all integers big-endian:
+ *
+ * u32 magic ("TKPV")
+ * u16 version (1)
+ * u16 attestation length, then the TPMS_ATTEST
+ * u8[32] AK signature R
+ * u8[32] AK signature S
+ * u8 number of commands, then for each command:
+ * u32 command code
+ * u8[32] cpHash
+ * u8[32] rpHash
+ * u16 response parameter length, then the parameters
+ */
+static int tpm2_kkey_serialize(struct tpm2_key_provenance *prov,
+ const struct tpm2_signed_audit *audit,
+ const struct tpm2_audit_entry *log,
+ const struct tpm2_kkey_rsp *rsp)
+{
+ struct tpm2_kkey_writer w = { .prov = prov };
+ int i;
+
+ prov->len = 0;
+ tpm2_kkey_put_u32(&w, TPM2_KKEY_PROV_MAGIC);
+ tpm2_kkey_put_u16(&w, TPM2_KKEY_PROV_VERSION);
+ tpm2_kkey_put_u16(&w, audit->attest_len);
+ tpm2_kkey_put(&w, audit->attest, audit->attest_len);
+ tpm2_kkey_put(&w, audit->sig_r, sizeof(audit->sig_r));
+ tpm2_kkey_put(&w, audit->sig_s, sizeof(audit->sig_s));
+ tpm2_kkey_put_u8(&w, TPM2_KKEY_NR_CMDS);
+
+ for (i = 0; i < TPM2_KKEY_NR_CMDS; i++) {
+ tpm2_kkey_put_u32(&w, rsp[i].cc);
+ tpm2_kkey_put(&w, log[i].cphash, sizeof(log[i].cphash));
+ tpm2_kkey_put(&w, log[i].rphash, sizeof(log[i].rphash));
+ tpm2_kkey_put_u16(&w, rsp[i].len);
+ tpm2_kkey_put(&w, rsp[i].data, rsp[i].len);
+ }
+
+ return w.err ? -E2BIG : 0;
+}
+
+/**
+ * tpm2_kernel_key_create() - create a kernel signing key with provenance
+ * @chip: the TPM chip
+ * @key: filled with the new key, which remains loaded in the TPM
+ * @prov: filled with evidence that the key was created by the kernel
+ *
+ * Creates an ECDSA P-256 signing key as described at the top of this
+ * file. The kernel NV index must be usable and the owner and endorsement
+ * hierarchies must have empty auth values. Any existing auth session is
+ * ended. The caller must hold the chip's ops lock, and must release the
+ * key with tpm2_kernel_key_destroy().
+ *
+ * Return:
+ * * 0 - OK
+ * * -errno - A system error
+ * * TPM_RC - A TPM error
+ */
+int tpm2_kernel_key_create(struct tpm_chip *chip, struct tpm2_kernel_key *key,
+ struct tpm2_key_provenance *prov)
+{
+ struct tpm2_kkey_rsp *rsp __free(kfree) = NULL;
+ struct tpm_buf *buf __free(kfree_sensitive) = NULL;
+ struct tpm2_signed_audit audit = { };
+ const struct tpm2_audit_entry *log;
+ bool magic = false;
+ u32 null_key = 0;
+ int rc, rc2;
+
+ memset(key, 0, sizeof(*key));
+ prov->len = 0;
+
+ rsp = kcalloc(TPM2_KKEY_NR_CMDS, sizeof(*rsp), GFP_KERNEL);
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!rsp || !buf)
+ return -ENOMEM;
+ tpm_buf_init(buf, TPM_BUFSIZE);
+
+ get_random_bytes(key->auth, sizeof(key->auth));
+
+ /* the index may have been removed by TPM2_Clear since boot */
+ rc = tpm2_kernel_nv_provision(chip);
+ if (rc)
+ goto out;
+
+ tpm2_end_auth_session(chip);
+
+ rc = tpm2_load_null(chip, &null_key);
+ if (rc)
+ goto out;
+
+ magic = true;
+ rc = tpm2_kernel_nv_set_magic(chip);
+ if (rc)
+ goto out;
+
+ /* setting the index used an ordinary session */
+ tpm2_end_auth_session(chip);
+ rc = tpm2_start_auth_session(chip, true);
+ if (rc)
+ goto out;
+
+ rc = tpm2_kkey_pcr_read(chip, buf, &rsp[0]);
+ if (!rc)
+ rc = tpm2_kkey_nv_read(chip, buf, &rsp[1]);
+ if (!rc)
+ rc = tpm2_kkey_create_loaded(chip, buf, null_key, key, &rsp[2]);
+ if (rc)
+ goto out;
+
+ rc = tpm2_get_signed_audit_digest(chip, NULL, 0, &audit);
+ if (rc)
+ goto out;
+
+ if (tpm2_get_audit_log(chip, &log) != TPM2_KKEY_NR_CMDS) {
+ rc = -EIO;
+ goto out;
+ }
+
+ rc = tpm2_kkey_serialize(prov, &audit, log, rsp);
+
+out:
+ tpm2_free_signed_audit(&audit);
+ tpm2_end_auth_session(chip);
+
+ if (magic) {
+ rc2 = tpm2_kernel_nv_clear(chip);
+ if (!rc)
+ rc = rc2;
+ }
+
+ if (null_key)
+ tpm2_flush_context(chip, null_key);
+
+ if (rc) {
+ if (key->handle)
+ tpm2_flush_context(chip, key->handle);
+ memzero_explicit(key, sizeof(*key));
+ prov->len = 0;
+ dev_err(&chip->dev, "failed to create kernel signing key: %d\n",
+ rc);
+ }
+
+ return rc;
+}
+EXPORT_SYMBOL_GPL(tpm2_kernel_key_create);
diff --git a/drivers/char/tpm/tpm2-sessions.c b/drivers/char/tpm/tpm2-sessions.c
index e5ef30238fbe..78457843084e 100644
--- a/drivers/char/tpm/tpm2-sessions.c
+++ b/drivers/char/tpm/tpm2-sessions.c
@@ -1073,7 +1073,7 @@ static int tpm2_parse_start_auth_session(struct tpm2_auth *auth,
return 0;
}
-static int tpm2_load_null(struct tpm_chip *chip, u32 *null_key)
+int tpm2_load_null(struct tpm_chip *chip, u32 *null_key)
{
unsigned int offset = 0; /* dummy offset for null seed context */
u8 name[SHA256_DIGEST_SIZE + 2];
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 35ba30a2674d..7ee553688906 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -358,6 +358,45 @@ struct tpm2_signed_audit {
u8 sig_s[EC_PT_SZ];
};
+/**
+ * struct tpm2_kernel_key - a signing key created by the kernel
+ * @handle: transient handle of the loaded key
+ * @auth: the key's random auth value, which never leaves the kernel
+ * @x: X coordinate of the P-256 public key
+ * @y: Y coordinate of the P-256 public key
+ */
+struct tpm2_kernel_key {
+ u32 handle;
+ u8 auth[SHA256_DIGEST_SIZE];
+ u8 x[EC_PT_SZ];
+ u8 y[EC_PT_SZ];
+};
+
+#define TPM2_KEY_PROVENANCE_MAX 2048
+
+/**
+ * struct tpm2_key_provenance - evidence that a key was created by the kernel
+ * @len: length of @data
+ * @data: serialized AK-signed audit session attestation and log, in a
+ * format private to the TPM driver
+ */
+struct tpm2_key_provenance {
+ u16 len;
+ u8 data[TPM2_KEY_PROVENANCE_MAX];
+};
+
+#ifdef CONFIG_TCG_TPM2_KERNEL_KEY
+int tpm2_kernel_key_create(struct tpm_chip *chip, struct tpm2_kernel_key *key,
+ struct tpm2_key_provenance *prov);
+#else
+static inline int tpm2_kernel_key_create(struct tpm_chip *chip,
+ struct tpm2_kernel_key *key,
+ struct tpm2_key_provenance *prov)
+{
+ return -EOPNOTSUPP;
+}
+#endif
+
#ifdef CONFIG_TCG_TPM2_HMAC
int tpm2_start_auth_session(struct tpm_chip *chip, bool audit);
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance
2026-10-08 13:20 ` [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance Matthew Garrett
@ 2026-10-08 17:00 ` James Bottomley
2026-10-09 8:31 ` Matthew Garrett
0 siblings, 1 reply; 31+ messages in thread
From: James Bottomley @ 2026-10-08 17:00 UTC (permalink / raw)
To: Matthew Garrett, mjg59
Cc: keyrings, linux-integrity, rafael, linux-pm, linux-efi
On Thu, 2026-10-08 at 06:20 -0700, Matthew Garrett wrote:
> Add tpm2_kernel_key_create(), which creates an ECDSA P-256 signing
> key for the kernel along with evidence that the kernel, rather than
> userspace, created it. The key is created with TPM2_CreateLoaded
I don't think you want to do this. TPM2_CreateLoaded was added in spec
version 1.38. A lot of current TPMs on the market today have spec
versions lower than this and would reject the command (The spec version
of the Nuvoton in my Dell XPS-13 is 1.16). Since it's really just a
shorthand for TPM2_Create followed by TPM2_Load, you can simply do that
instead, which would save us from a load of complaints about this not
working.
Regards,
James
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance
2026-10-08 17:00 ` James Bottomley
@ 2026-10-09 8:31 ` Matthew Garrett
0 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-09 8:31 UTC (permalink / raw)
To: James Bottomley
Cc: Matthew Garrett, keyrings, linux-integrity, rafael, linux-pm,
linux-efi
On Thu, Oct 08, 2026 at 07:00:28PM +0200, James Bottomley wrote:
> On Thu, 2026-10-08 at 06:20 -0700, Matthew Garrett wrote:
> > Add tpm2_kernel_key_create(), which creates an ECDSA P-256 signing
> > key for the kernel along with evidence that the kernel, rather than
> > userspace, created it. The key is created with TPM2_CreateLoaded
>
> I don't think you want to do this. TPM2_CreateLoaded was added in spec
> version 1.38. A lot of current TPMs on the market today have spec
> versions lower than this and would reject the command (The spec version
> of the Nuvoton in my Dell XPS-13 is 1.16). Since it's really just a
> shorthand for TPM2_Create followed by TPM2_Load, you can simply do that
> instead, which would save us from a load of complaints about this not
> working.
This is what I get for working to the spec instead of reality. Thanks,
I'll split that up.
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH 12/17] tpm: Add signing with the kernel signing key
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (10 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 11/17] tpm: Add kernel signing key creation with audited provenance Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 13/17] tpm: Add verification of kernel signing key provenance Matthew Garrett
` (5 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm2_kernel_key_sign(), which signs a SHA-256 digest with a key
created by tpm2_kernel_key_create() using TPM2_Sign, and
tpm2_kernel_key_destroy(), which flushes the key and erases its auth
value.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/tpm2-kernel-key.c | 98 ++++++++++++++++++++++++++++++
include/linux/tpm.h | 16 +++++
include/linux/tpm_command.h | 2 +
3 files changed, 116 insertions(+)
diff --git a/drivers/char/tpm/tpm2-kernel-key.c b/drivers/char/tpm/tpm2-kernel-key.c
index 27815525bc3f..89c11c78097a 100644
--- a/drivers/char/tpm/tpm2-kernel-key.c
+++ b/drivers/char/tpm/tpm2-kernel-key.c
@@ -402,3 +402,101 @@ int tpm2_kernel_key_create(struct tpm_chip *chip, struct tpm2_kernel_key *key,
return rc;
}
EXPORT_SYMBOL_GPL(tpm2_kernel_key_create);
+
+/**
+ * tpm2_kernel_key_sign() - sign a digest with a kernel signing key
+ * @chip: the TPM chip
+ * @key: a key created by tpm2_kernel_key_create()
+ * @digest: the SHA-256 digest to sign
+ * @r: filled with the R component of the ECDSA signature
+ * @s: filled with the S component of the ECDSA signature
+ *
+ * The key is authorized with its auth value through an HMAC session, so
+ * the auth value is never sent to the TPM in the clear. The caller must
+ * hold the chip's ops lock.
+ *
+ * Return:
+ * * 0 - OK
+ * * -errno - A system error
+ * * TPM_RC - A TPM error
+ */
+int tpm2_kernel_key_sign(struct tpm_chip *chip, struct tpm2_kernel_key *key,
+ const u8 digest[SHA256_DIGEST_SIZE],
+ u8 r[EC_PT_SZ], u8 s[EC_PT_SZ])
+{
+ struct tpm_buf *buf __free(kfree) = NULL;
+ struct tpm2_rsp rsp;
+ int rc;
+
+ rc = tpm2_start_auth_session(chip, false);
+ if (rc && rc != -EBUSY)
+ return rc;
+
+ buf = kzalloc(TPM_BUFSIZE, GFP_KERNEL);
+ if (!buf) {
+ tpm2_end_auth_session(chip);
+ return -ENOMEM;
+ }
+
+ tpm_buf_init(buf, TPM_BUFSIZE);
+ tpm_buf_reset(buf, TPM2_ST_SESSIONS, TPM2_CC_SIGN);
+
+ rc = tpm_buf_append_name(chip, buf, key->handle, NULL);
+ if (rc)
+ return rc;
+
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION,
+ key->auth, sizeof(key->auth));
+
+ /* digest */
+ tpm_buf_append_u16(buf, SHA256_DIGEST_SIZE);
+ tpm_buf_append(buf, digest, SHA256_DIGEST_SIZE);
+ /* inScheme: use the key's scheme */
+ tpm_buf_append_u16(buf, TPM_ALG_NULL);
+ /* validation: a NULL ticket, as the key is not restricted */
+ tpm_buf_append_u16(buf, TPM2_ST_HASHCHECK);
+ tpm_buf_append_u32(buf, TPM2_RH_NULL);
+ tpm_buf_append_u16(buf, 0);
+
+ rc = tpm_buf_fill_hmac_session(chip, buf);
+ if (rc)
+ return rc;
+
+ rc = tpm_transmit_cmd(chip, buf, 0, "signing with kernel key");
+ rc = tpm_buf_check_hmac_response(chip, buf, rc);
+ if (rc) {
+ if (rc < 0)
+ tpm2_end_auth_session(chip);
+ return rc;
+ }
+
+ tpm2_rsp_init(&rsp, buf);
+ /* parameterSize */
+ tpm2_rsp_u32(&rsp);
+ if (tpm2_rsp_u16(&rsp) != TPM_ALG_ECDSA ||
+ tpm2_rsp_u16(&rsp) != TPM_ALG_SHA256)
+ return -EIO;
+ tpm2_rsp_ecc_param(&rsp, r);
+ tpm2_rsp_ecc_param(&rsp, s);
+
+ return rsp.err ? -EIO : 0;
+}
+EXPORT_SYMBOL_GPL(tpm2_kernel_key_sign);
+
+/**
+ * tpm2_kernel_key_destroy() - unload a kernel signing key
+ * @chip: the TPM chip
+ * @key: a key created by tpm2_kernel_key_create()
+ *
+ * Flushes the key from the TPM and erases its auth value. The caller must
+ * hold the chip's ops lock.
+ */
+void tpm2_kernel_key_destroy(struct tpm_chip *chip,
+ struct tpm2_kernel_key *key)
+{
+ if (key->handle)
+ tpm2_flush_context(chip, key->handle);
+
+ memzero_explicit(key, sizeof(*key));
+}
+EXPORT_SYMBOL_GPL(tpm2_kernel_key_destroy);
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 7ee553688906..505ea6f4bb46 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -388,6 +388,11 @@ struct tpm2_key_provenance {
#ifdef CONFIG_TCG_TPM2_KERNEL_KEY
int tpm2_kernel_key_create(struct tpm_chip *chip, struct tpm2_kernel_key *key,
struct tpm2_key_provenance *prov);
+int tpm2_kernel_key_sign(struct tpm_chip *chip, struct tpm2_kernel_key *key,
+ const u8 digest[SHA256_DIGEST_SIZE],
+ u8 r[EC_PT_SZ], u8 s[EC_PT_SZ]);
+void tpm2_kernel_key_destroy(struct tpm_chip *chip,
+ struct tpm2_kernel_key *key);
#else
static inline int tpm2_kernel_key_create(struct tpm_chip *chip,
struct tpm2_kernel_key *key,
@@ -395,6 +400,17 @@ static inline int tpm2_kernel_key_create(struct tpm_chip *chip,
{
return -EOPNOTSUPP;
}
+static inline int tpm2_kernel_key_sign(struct tpm_chip *chip,
+ struct tpm2_kernel_key *key,
+ const u8 digest[SHA256_DIGEST_SIZE],
+ u8 r[EC_PT_SZ], u8 s[EC_PT_SZ])
+{
+ return -EOPNOTSUPP;
+}
+static inline void tpm2_kernel_key_destroy(struct tpm_chip *chip,
+ struct tpm2_kernel_key *key)
+{
+}
#endif
#ifdef CONFIG_TCG_TPM2_HMAC
diff --git a/include/linux/tpm_command.h b/include/linux/tpm_command.h
index cef85f532b6c..9ff2f67c55c6 100644
--- a/include/linux/tpm_command.h
+++ b/include/linux/tpm_command.h
@@ -191,6 +191,7 @@ enum tpm2_structures {
TPM2_ST_SESSIONS = 0x8002,
TPM2_ST_ATTEST_SESSION_AUDIT = 0x8016,
TPM2_ST_CREATION = 0x8021,
+ TPM2_ST_HASHCHECK = 0x8024,
};
/* Indicates from what layer of the software stack the error comes from */
@@ -238,6 +239,7 @@ enum tpm2_command_codes {
TPM2_CC_CREATE = 0x0153,
TPM2_CC_LOAD = 0x0157,
TPM2_CC_SEQUENCE_UPDATE = 0x015C,
+ TPM2_CC_SIGN = 0x015D,
TPM2_CC_UNSEAL = 0x015E,
TPM2_CC_CONTEXT_LOAD = 0x0161,
TPM2_CC_CONTEXT_SAVE = 0x0162,
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 13/17] tpm: Add verification of kernel signing key provenance
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (11 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 12/17] tpm: Add signing with the kernel signing key Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 14/17] PM: hibernate: Add image digest and signature page infrastructure Matthew Garrett
` (4 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add tpm2_kernel_key_verify(), which checks the provenance produced by
tpm2_kernel_key_create() and returns the public key it shows the kernel
created. It checks that:
* The session audit attestation is signed by this TPM's kernel AK
* Replaying the logged cpHash and rpHash pairs from a zero digest
reproduces the attested session digest
* Each logged rpHash matches the recorded response parameters, and the
commands were PCR_Read, NV_Read, and CreateLoaded in that order
* PCR 5 was read, and held the value it holds now
* The read of the NV index returned the magic value
* The key created has exactly the attributes of a kernel signing key.
Since userspace cannot store the magic value in the kernel NV index,
this shows that the key was created while the kernel was in control of
the TPM.
Also add tpm2_kernel_key_verify_signature(), which verifies an ECDSA
P-256 signature in software with a given public key, for checking data
signed with a kernel signing key.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
drivers/char/tpm/Kconfig | 1 +
drivers/char/tpm/tpm2-kernel-key.c | 372 +++++++++++++++++++++++++++++
include/linux/tpm.h | 21 ++
3 files changed, 394 insertions(+)
diff --git a/drivers/char/tpm/Kconfig b/drivers/char/tpm/Kconfig
index 15d204f8fa6d..c7ad2fef6d23 100644
--- a/drivers/char/tpm/Kconfig
+++ b/drivers/char/tpm/Kconfig
@@ -45,6 +45,7 @@ config TCG_TPM2_HMAC
config TCG_TPM2_KERNEL_KEY
bool "Kernel-generated TPM signing keys with provenance"
depends on TCG_TPM2_HMAC
+ select CRYPTO_ECDSA
help
Allow the kernel to create TPM signing keys along with evidence,
signed by the kernel attestation key, that the key was created
diff --git a/drivers/char/tpm/tpm2-kernel-key.c b/drivers/char/tpm/tpm2-kernel-key.c
index 89c11c78097a..3f9f6426a8a4 100644
--- a/drivers/char/tpm/tpm2-kernel-key.c
+++ b/drivers/char/tpm/tpm2-kernel-key.c
@@ -31,6 +31,8 @@
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
+#include <crypto/sha2.h>
+#include <crypto/sig.h>
#include "tpm.h"
#include "tpm2-rsp.h"
@@ -59,6 +61,16 @@ static const u8 tpm2_kkey_pcr5_select[] = {
0x20, 0x00, 0x00, /* PCR 5 */
};
+static const u8 tpm2_kkey_pcr5_bitmap[] = { 0x20, 0x00, 0x00 };
+
+static const struct {
+ u16 alg;
+ u16 size;
+} tpm2_kkey_pcr5_banks[] = {
+ { TPM_ALG_SHA1, SHA1_DIGEST_SIZE },
+ { TPM_ALG_SHA256, SHA256_DIGEST_SIZE },
+};
+
/* The parameters of a response, as covered by the logged rpHash */
struct tpm2_kkey_rsp {
u32 cc;
@@ -500,3 +512,363 @@ void tpm2_kernel_key_destroy(struct tpm_chip *chip,
memzero_explicit(key, sizeof(*key));
}
EXPORT_SYMBOL_GPL(tpm2_kernel_key_destroy);
+
+/**
+ * tpm2_kernel_key_verify_signature() - verify an ECDSA P-256 signature
+ * @x: X coordinate of the public key
+ * @y: Y coordinate of the public key
+ * @digest: the SHA-256 digest that was signed
+ * @r: R component of the signature
+ * @s: S component of the signature
+ *
+ * Return: 0 if the signature is valid, -EKEYREJECTED if it is not, or
+ * another -errno on failure.
+ */
+int tpm2_kernel_key_verify_signature(const u8 x[EC_PT_SZ],
+ const u8 y[EC_PT_SZ],
+ const u8 digest[SHA256_DIGEST_SIZE],
+ const u8 r[EC_PT_SZ],
+ const u8 s[EC_PT_SZ])
+{
+ u8 pub[1 + 2 * EC_PT_SZ], sig[2 * EC_PT_SZ];
+ struct crypto_sig *tfm;
+ int rc;
+
+ tfm = crypto_alloc_sig("p1363(ecdsa-nist-p256)", 0, 0);
+ if (IS_ERR(tfm))
+ return PTR_ERR(tfm);
+
+ /* uncompressed point */
+ pub[0] = 0x04;
+ memcpy(&pub[1], x, EC_PT_SZ);
+ memcpy(&pub[1 + EC_PT_SZ], y, EC_PT_SZ);
+ memcpy(sig, r, EC_PT_SZ);
+ memcpy(&sig[EC_PT_SZ], s, EC_PT_SZ);
+
+ rc = crypto_sig_set_pubkey(tfm, pub, sizeof(pub));
+ if (!rc)
+ rc = crypto_sig_verify(tfm, sig, sizeof(sig), digest,
+ SHA256_DIGEST_SIZE);
+
+ crypto_free_sig(tfm);
+ return rc == -EBADMSG ? -EKEYREJECTED : rc;
+}
+EXPORT_SYMBOL_GPL(tpm2_kernel_key_verify_signature);
+
+/* A logged command, parsed from the serialized provenance */
+struct tpm2_kkey_entry {
+ u32 cc;
+ const u8 *cphash;
+ const u8 *rphash;
+ const u8 *params;
+ u16 len;
+};
+
+static const u32 tpm2_kkey_expected_cc[TPM2_KKEY_NR_CMDS] = {
+ TPM2_CC_PCR_READ,
+ TPM2_CC_NV_READ,
+ TPM2_CC_CREATE_LOADED,
+};
+
+/* Check that the attestation is a session audit, and find its digest */
+static const u8 *tpm2_kkey_attest_digest(const u8 *attest, u16 len)
+{
+ struct tpm2_rsp r;
+ const u8 *digest;
+ u16 size;
+
+ tpm2_rsp_init_data(&r, attest, len);
+ if (tpm2_rsp_u32(&r) != TPM2_GENERATED_VALUE ||
+ tpm2_rsp_u16(&r) != TPM2_ST_ATTEST_SESSION_AUDIT)
+ return NULL;
+
+ /* qualifiedSigner, extraData */
+ tpm2_rsp_tpm2b(&r, &size);
+ tpm2_rsp_tpm2b(&r, &size);
+ /* clockInfo (clock, resetCount, restartCount, safe), firmwareVersion */
+ tpm2_rsp_bytes(&r, 8 + 4 + 4 + 1);
+ tpm2_rsp_bytes(&r, 8);
+ /* exclusiveSession */
+ tpm2_rsp_u8(&r);
+ digest = tpm2_rsp_tpm2b(&r, &size);
+
+ if (r.err || r.off != r.len || size != SHA256_DIGEST_SIZE)
+ return NULL;
+
+ return digest;
+}
+
+static bool tpm2_kkey_bank_allocated(struct tpm_chip *chip, u16 alg)
+{
+ int i;
+
+ for (i = 0; i < chip->nr_allocated_banks; i++)
+ if (chip->allocated_banks[i].alg_id == alg)
+ return true;
+
+ return false;
+}
+
+/*
+ * Check a PCR_Read of PCR 5 against its current values. Every bank that
+ * was read must hold the same value now, and the banks read must be
+ * exactly the SHA-1 and SHA-256 banks that are allocated. A TPM leaves
+ * an unallocated bank out of the response or returns it with an empty
+ * selection, and since the response is covered by the attested audit
+ * digest, the set of banks read cannot be altered.
+ */
+static int tpm2_kkey_check_pcr5(struct tpm_chip *chip,
+ const struct tpm2_kkey_entry *e)
+{
+ bool read[ARRAY_SIZE(tpm2_kkey_pcr5_banks)] = { };
+ u8 cphash[SHA256_DIGEST_SIZE];
+ __be32 cc = cpu_to_be32(e->cc);
+ unsigned int nr_read = 0;
+ struct sha256_ctx ctx;
+ struct tpm2_rsp r;
+ int i, j, prev = -1, rc;
+ u32 count;
+
+ sha256_init(&ctx);
+ sha256_update(&ctx, (u8 *)&cc, sizeof(cc));
+ sha256_update(&ctx, tpm2_kkey_pcr5_select,
+ sizeof(tpm2_kkey_pcr5_select));
+ sha256_final(&ctx, cphash);
+ if (memcmp(cphash, e->cphash, sizeof(cphash)))
+ return -EKEYREJECTED;
+
+ tpm2_rsp_init_data(&r, e->params, e->len);
+ /* pcrUpdateCounter */
+ tpm2_rsp_u32(&r);
+
+ /* pcrSelectionOut: which banks were actually read, in order */
+ count = tpm2_rsp_u32(&r);
+ if (count > ARRAY_SIZE(tpm2_kkey_pcr5_banks))
+ return -EKEYREJECTED;
+
+ for (i = 0; i < count; i++) {
+ u16 alg = tpm2_rsp_u16(&r);
+ const u8 *bitmap;
+
+ if (tpm2_rsp_u8(&r) != sizeof(tpm2_kkey_pcr5_bitmap))
+ return -EKEYREJECTED;
+ bitmap = tpm2_rsp_bytes(&r, sizeof(tpm2_kkey_pcr5_bitmap));
+ if (!bitmap)
+ return -EKEYREJECTED;
+
+ for (j = 0; j < ARRAY_SIZE(tpm2_kkey_pcr5_banks); j++)
+ if (tpm2_kkey_pcr5_banks[j].alg == alg)
+ break;
+ /* banks must be distinct and in the order requested */
+ if (j == ARRAY_SIZE(tpm2_kkey_pcr5_banks) || j <= prev)
+ return -EKEYREJECTED;
+ prev = j;
+
+ if (!memcmp(bitmap, tpm2_kkey_pcr5_bitmap,
+ sizeof(tpm2_kkey_pcr5_bitmap))) {
+ read[j] = true;
+ nr_read++;
+ } else if (memchr_inv(bitmap, 0, sizeof(tpm2_kkey_pcr5_bitmap))) {
+ return -EKEYREJECTED;
+ }
+ }
+
+ if (!nr_read)
+ return -EKEYREJECTED;
+
+ /* TPML_DIGEST, one digest per bank read, in the same order */
+ if (tpm2_rsp_u32(&r) != nr_read)
+ return -EKEYREJECTED;
+
+ for (j = 0; j < ARRAY_SIZE(tpm2_kkey_pcr5_banks); j++) {
+ struct tpm_digest pcr = { .alg_id = tpm2_kkey_pcr5_banks[j].alg };
+ const u8 *digest;
+ u16 size;
+
+ /* every allocated bank must have been read */
+ if (!read[j]) {
+ if (tpm2_kkey_bank_allocated(chip, pcr.alg_id))
+ return -EKEYREJECTED;
+ continue;
+ }
+
+ digest = tpm2_rsp_tpm2b(&r, &size);
+ if (!digest || size != tpm2_kkey_pcr5_banks[j].size)
+ return -EKEYREJECTED;
+
+ rc = tpm2_pcr_read(chip, 5, &pcr, NULL);
+ if (rc)
+ return rc;
+
+ if (memcmp(digest, pcr.digest, size))
+ return -EKEYREJECTED;
+ }
+
+ return r.err || r.off != r.len ? -EKEYREJECTED : 0;
+}
+
+/* Check an NV_Read of the kernel index, and that it returned the magic */
+static int tpm2_kkey_check_nv_read(const struct tpm2_kkey_entry *e)
+{
+ u8 name[TPM2_NULL_NAME_SIZE], cphash[SHA256_DIGEST_SIZE];
+ struct sha256_ctx ctx;
+ __be32 cc = cpu_to_be32(e->cc);
+ __be16 size = cpu_to_be16(TPM2_KERNEL_NV_SIZE), offset = 0;
+
+ /* authHandle and nvIndex are both the index, as last written */
+ tpm2_kernel_nv_name(true, name);
+ sha256_init(&ctx);
+ sha256_update(&ctx, (u8 *)&cc, sizeof(cc));
+ sha256_update(&ctx, name, sizeof(name));
+ sha256_update(&ctx, name, sizeof(name));
+ sha256_update(&ctx, (u8 *)&size, sizeof(size));
+ sha256_update(&ctx, (u8 *)&offset, sizeof(offset));
+ sha256_final(&ctx, cphash);
+
+ if (memcmp(cphash, e->cphash, sizeof(cphash)))
+ return -EKEYREJECTED;
+
+ if (e->len != sizeof(u16) + TPM2_KERNEL_NV_SIZE ||
+ get_unaligned_be16(e->params) != TPM2_KERNEL_NV_SIZE ||
+ memcmp(e->params + sizeof(u16), tpm2_kernel_nv_magic,
+ TPM2_KERNEL_NV_SIZE))
+ return -EKEYREJECTED;
+
+ return 0;
+}
+
+/* Extract the public key from a CreateLoaded response */
+static int tpm2_kkey_check_create(const struct tpm2_kkey_entry *e,
+ u8 x[EC_PT_SZ], u8 y[EC_PT_SZ])
+{
+ struct tpm2_rsp r;
+ u16 size;
+
+ tpm2_rsp_init_data(&r, e->params, e->len);
+ /* outPrivate */
+ tpm2_rsp_tpm2b(&r, &size);
+ if (tpm2_rsp_ecdsa_public(&r, TPM2_OA_KERNEL_KEY, x, y))
+ return -EKEYREJECTED;
+ /* name */
+ tpm2_rsp_tpm2b(&r, &size);
+
+ return r.err || r.off != r.len ? -EKEYREJECTED : 0;
+}
+
+/**
+ * tpm2_kernel_key_verify() - verify the provenance of a kernel signing key
+ * @chip: the TPM chip
+ * @prov: provenance returned by tpm2_kernel_key_create()
+ * @x: filled with the X coordinate of the key's public key
+ * @y: filled with the Y coordinate of the key's public key
+ *
+ * Checks that @prov shows the key was created by the kernel:
+ *
+ * - the attestation is signed by this TPM's kernel AK;
+ * - the logged commands reproduce the attested audit digest;
+ * - the logged responses match the recorded response parameters, and the
+ * commands were PCR_Read, NV_Read, and CreateLoaded, in order;
+ * - PCR 5 was read and held the same value as it does now;
+ * - the kernel NV index was read, and returned the magic value;
+ * - the key created has the attributes of a kernel signing key.
+ *
+ * The caller must hold the chip's ops lock.
+ *
+ * Return: 0 if the provenance is valid, -EKEYREJECTED if it is not, or
+ * another -errno or a TPM error code on failure.
+ */
+int tpm2_kernel_key_verify(struct tpm_chip *chip,
+ const struct tpm2_key_provenance *prov,
+ u8 x[EC_PT_SZ], u8 y[EC_PT_SZ])
+{
+ struct tpm2_kkey_entry e[TPM2_KKEY_NR_CMDS];
+ u8 ak_x[EC_PT_SZ], ak_y[EC_PT_SZ];
+ u8 digest[SHA256_DIGEST_SIZE];
+ const u8 *attest, *sig_r, *sig_s, *session_digest;
+ struct sha256_ctx ctx;
+ struct tpm2_rsp r;
+ u16 attest_len;
+ u32 ak;
+ int i, rc;
+
+ if (prov->len > sizeof(prov->data))
+ return -EKEYREJECTED;
+
+ tpm2_rsp_init_data(&r, prov->data, prov->len);
+ if (tpm2_rsp_u32(&r) != TPM2_KKEY_PROV_MAGIC ||
+ tpm2_rsp_u16(&r) != TPM2_KKEY_PROV_VERSION)
+ return -EKEYREJECTED;
+
+ attest = tpm2_rsp_tpm2b(&r, &attest_len);
+ sig_r = tpm2_rsp_bytes(&r, EC_PT_SZ);
+ sig_s = tpm2_rsp_bytes(&r, EC_PT_SZ);
+ if (tpm2_rsp_u8(&r) != TPM2_KKEY_NR_CMDS)
+ return -EKEYREJECTED;
+
+ for (i = 0; i < TPM2_KKEY_NR_CMDS; i++) {
+ e[i].cc = tpm2_rsp_u32(&r);
+ e[i].cphash = tpm2_rsp_bytes(&r, SHA256_DIGEST_SIZE);
+ e[i].rphash = tpm2_rsp_bytes(&r, SHA256_DIGEST_SIZE);
+ e[i].params = tpm2_rsp_tpm2b(&r, &e[i].len);
+ if (e[i].cc != tpm2_kkey_expected_cc[i])
+ return -EKEYREJECTED;
+ }
+
+ if (r.err || r.off != r.len)
+ return -EKEYREJECTED;
+
+ /* the attestation must be signed by this TPM's kernel AK */
+ rc = tpm2_create_kernel_ak(chip, &ak, ak_x, ak_y);
+ if (rc)
+ return rc;
+ tpm2_flush_context(chip, ak);
+
+ sha256(attest, attest_len, digest);
+ rc = tpm2_kernel_key_verify_signature(ak_x, ak_y, digest, sig_r,
+ sig_s);
+ if (rc)
+ return rc;
+
+ session_digest = tpm2_kkey_attest_digest(attest, attest_len);
+ if (!session_digest)
+ return -EKEYREJECTED;
+
+ /* replay the log from the initial zero digest */
+ memset(digest, 0, sizeof(digest));
+ for (i = 0; i < TPM2_KKEY_NR_CMDS; i++) {
+ u8 rphash[SHA256_DIGEST_SIZE];
+ __be32 cc = cpu_to_be32(e[i].cc);
+ __be32 rc_success = 0;
+
+ /* the response must match the recorded parameters */
+ sha256_init(&ctx);
+ sha256_update(&ctx, (u8 *)&rc_success, sizeof(rc_success));
+ sha256_update(&ctx, (u8 *)&cc, sizeof(cc));
+ sha256_update(&ctx, e[i].params, e[i].len);
+ sha256_final(&ctx, rphash);
+ if (memcmp(rphash, e[i].rphash, sizeof(rphash)))
+ return -EKEYREJECTED;
+
+ sha256_init(&ctx);
+ sha256_update(&ctx, digest, sizeof(digest));
+ sha256_update(&ctx, e[i].cphash, SHA256_DIGEST_SIZE);
+ sha256_update(&ctx, e[i].rphash, SHA256_DIGEST_SIZE);
+ sha256_final(&ctx, digest);
+ }
+
+ if (memcmp(digest, session_digest, sizeof(digest)))
+ return -EKEYREJECTED;
+
+ /* PCR 5 must hold the same value now */
+ rc = tpm2_kkey_check_pcr5(chip, &e[0]);
+ if (rc)
+ return rc;
+
+ /* the key was created after a read of the magic value */
+ rc = tpm2_kkey_check_nv_read(&e[1]);
+ if (!rc)
+ rc = tpm2_kkey_check_create(&e[2], x, y);
+
+ return rc;
+}
+EXPORT_SYMBOL_GPL(tpm2_kernel_key_verify);
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 505ea6f4bb46..9be88f1cc2aa 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -393,6 +393,14 @@ int tpm2_kernel_key_sign(struct tpm_chip *chip, struct tpm2_kernel_key *key,
u8 r[EC_PT_SZ], u8 s[EC_PT_SZ]);
void tpm2_kernel_key_destroy(struct tpm_chip *chip,
struct tpm2_kernel_key *key);
+int tpm2_kernel_key_verify(struct tpm_chip *chip,
+ const struct tpm2_key_provenance *prov,
+ u8 x[EC_PT_SZ], u8 y[EC_PT_SZ]);
+int tpm2_kernel_key_verify_signature(const u8 x[EC_PT_SZ],
+ const u8 y[EC_PT_SZ],
+ const u8 digest[SHA256_DIGEST_SIZE],
+ const u8 r[EC_PT_SZ],
+ const u8 s[EC_PT_SZ]);
#else
static inline int tpm2_kernel_key_create(struct tpm_chip *chip,
struct tpm2_kernel_key *key,
@@ -411,6 +419,19 @@ static inline void tpm2_kernel_key_destroy(struct tpm_chip *chip,
struct tpm2_kernel_key *key)
{
}
+static inline int tpm2_kernel_key_verify(struct tpm_chip *chip,
+ const struct tpm2_key_provenance *prov,
+ u8 x[EC_PT_SZ], u8 y[EC_PT_SZ])
+{
+ return -EOPNOTSUPP;
+}
+static inline int
+tpm2_kernel_key_verify_signature(const u8 x[EC_PT_SZ], const u8 y[EC_PT_SZ],
+ const u8 digest[SHA256_DIGEST_SIZE],
+ const u8 r[EC_PT_SZ], const u8 s[EC_PT_SZ])
+{
+ return -EOPNOTSUPP;
+}
#endif
#ifdef CONFIG_TCG_TPM2_HMAC
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 14/17] PM: hibernate: Add image digest and signature page infrastructure
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (12 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 13/17] tpm: Add verification of kernel signing key provenance Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 15/17] PM: hibernate: Sign and verify images with a kernel-generated TPM key Matthew Garrett
` (3 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add support for signing hibernation images, so that a kernel can refuse
to restore an image that it did not write or that has been modified.
A signing backend registers a struct hib_sig_ops. When one is
registered, the SHA-256 digest of every page of the image stream, with
a domain separation prefix, is computed as the image is produced by
snapshot_read_next(). An extra page holding the backend's signature over
the digest is added to the end of the stream, and the image header
records its presence along with up to HIB_SIG_HEADER_SIZE bytes of
backend data needed for verification. As the signature page is part of
the image stream, it is carried by every backend that stores the
stream, including uswsusp.
When the image is loaded, snapshot_write_next() hashes each page once
the caller has filled it, skipping zero pages that are reconstructed
rather than read. The backend checks its header data as soon as the
header is loaded, before the rest of the image is read, and the
signature is verified at the start of hibernation_restore(), while
devices are still usable and before anything is quiesced. With a
backend registered, an image without a signature is rejected.
The uncompressed swap reader issues reads asynchronously, so when the
image is signed it now waits for each page before passing it on to be
hashed. The compressed reader and uswsusp already copy each page
synchronously.
With no backend registered, there is no change in behaviour.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
kernel/power/Kconfig | 1 +
kernel/power/hibernate.c | 6 ++
kernel/power/power.h | 34 +++++++++
kernel/power/snapshot.c | 161 ++++++++++++++++++++++++++++++++++++---
kernel/power/swap.c | 3 +-
5 files changed, 194 insertions(+), 11 deletions(-)
diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index 71165e7f04f4..846f0f91dbc0 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -43,6 +43,7 @@ config HIBERNATION
select CRYPTO
select CRYPTO_LZO
select CRYPTO_LZ4
+ select CRYPTO_LIB_SHA256
help
Enable the suspend to disk (STD) functionality, which is usually
called "hibernation" in user interfaces. STD checkpoints the
diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c
index c13f68ab7f6e..1efc2bd3a387 100644
--- a/kernel/power/hibernate.c
+++ b/kernel/power/hibernate.c
@@ -568,6 +568,12 @@ int hibernation_restore(int platform_mode)
{
int error;
+ error = snapshot_image_verify();
+ if (error) {
+ pr_err("Image signature verification failed: %d\n", error);
+ return error;
+ }
+
pm_prepare_console();
console_suspend_all();
error = dpm_suspend_start(PMSG_QUIESCE);
diff --git a/kernel/power/power.h b/kernel/power/power.h
index 75b63843886e..b4733d45e6cd 100644
--- a/kernel/power/power.h
+++ b/kernel/power/power.h
@@ -8,6 +8,9 @@
#include <linux/cpuidle.h>
#include <linux/crypto.h>
+#define HIB_SIG_HEADER_SIZE 2560
+#define HIB_SIG_TRAILER_SIZE 256
+
struct swsusp_info {
struct new_utsname uts;
u32 version_code;
@@ -16,8 +19,36 @@ struct swsusp_info {
unsigned long image_pages;
unsigned long pages;
unsigned long size;
+ /* number of signature pages at the end of the image (0 or 1) */
+ unsigned long sig_pages;
+ /* data needed to verify the signature, stored by hib_sig_ops.begin */
+ u8 sig_header[HIB_SIG_HEADER_SIZE];
} __aligned(PAGE_SIZE);
+/**
+ * struct hib_sig_ops - hibernation image signing
+ * @begin: Called when the image header is built. May store up to
+ * @size bytes needed to verify the signature in @data.
+ * @sign: Sign @digest, the SHA-256 digest of every page of the image
+ * before the signature page, storing the signature in @trailer.
+ * @end: Release any resources held for signing, after signing or when
+ * hibernation is aborted. May be called more than once.
+ * @check_header: Check the data stored by @begin when the image header is
+ * loaded, before the rest of the image is read.
+ * @verify: Verify the signature in @trailer over @digest before the
+ * image is restored.
+ *
+ * If signing is enabled, every image is signed and images without a valid
+ * signature are not restored.
+ */
+struct hib_sig_ops {
+ int (*begin)(void *data, size_t size);
+ int (*sign)(const u8 *digest, void *trailer, size_t size);
+ void (*end)(void);
+ int (*check_header)(const void *data, size_t size);
+ int (*verify)(const u8 *digest, const void *trailer, size_t size);
+};
+
#if defined(CONFIG_SUSPEND) || defined(CONFIG_HIBERNATION)
extern int pm_sleep_fs_sync(void);
extern bool filesystem_freeze_enabled;
@@ -164,6 +195,9 @@ extern int snapshot_read_next(struct snapshot_handle *handle);
extern int snapshot_write_next(struct snapshot_handle *handle);
int snapshot_write_finalize(struct snapshot_handle *handle);
extern int snapshot_image_loaded(struct snapshot_handle *handle);
+extern bool snapshot_image_signed(void);
+extern int snapshot_image_verify(void);
+extern void hibernate_set_sig_ops(const struct hib_sig_ops *ops);
extern bool hibernate_acquire(void);
extern void hibernate_release(void);
diff --git a/kernel/power/snapshot.c b/kernel/power/snapshot.c
index b209712cb2c3..bbf9de4b15ab 100644
--- a/kernel/power/snapshot.c
+++ b/kernel/power/snapshot.c
@@ -36,6 +36,7 @@
#include <asm/mmu_context.h>
#include <asm/tlbflush.h>
#include <asm/io.h>
+#include <crypto/sha2.h>
#include "power.h"
@@ -168,6 +169,37 @@ static struct linked_page *safe_pages_list;
/* Pointer to an auxiliary buffer (1 page) */
static void *buffer;
+/*
+ * Image signing. When signing is enabled, the SHA-256 digest of every page
+ * of the image is signed, and the signature is stored in an extra page at
+ * the end of the image.
+ */
+static const struct hib_sig_ops *hib_sig;
+static unsigned long nr_sig_pages;
+static struct sha256_ctx hib_sig_ctx;
+static u8 hib_sig_digest[SHA256_DIGEST_SIZE];
+/*
+ * restore: the signature page is read here, as the image buffer is freed
+ * by prepare_image() on systems without highmem
+ */
+static u8 hib_sig_page[PAGE_SIZE] __aligned(PAGE_SIZE);
+/* restore: the page handed out last must be hashed when it is filled */
+static bool hib_sig_hash_pending;
+/* the signature page has been handed out, or (restore) received */
+static bool hib_sig_trailer_out;
+static bool hib_sig_trailer_in;
+
+static const char hib_sig_domain[] = "Linux hibernation image v1";
+
+static void hib_sig_start(void)
+{
+ sha256_init(&hib_sig_ctx);
+ sha256_update(&hib_sig_ctx, hib_sig_domain, sizeof(hib_sig_domain));
+ hib_sig_hash_pending = false;
+ hib_sig_trailer_out = false;
+ hib_sig_trailer_in = false;
+}
+
#define PG_ANY 0
#define PG_SAFE 1
#define PG_UNSAFE_CLEAR 1
@@ -1591,6 +1623,9 @@ void swsusp_free(void)
{
unsigned long fb_pfn, fr_pfn;
+ if (hib_sig && hib_sig->end)
+ hib_sig->end();
+
if (!forbidden_pages_map || !free_pages_map)
goto out;
@@ -2141,6 +2176,7 @@ asmlinkage __visible int swsusp_save(void)
/* We don't actually copy the zero pages */
nr_zero_pages = nr_pages - nr_copy_pages;
nr_meta_pages = DIV_ROUND_UP(nr_pages * sizeof(long), PAGE_SIZE);
+ nr_sig_pages = hib_sig ? 1 : 0;
pm_deferred_pr_dbg("Image created (%d pages copied, %d zero pages)\n",
nr_copy_pages, nr_zero_pages);
@@ -2174,17 +2210,26 @@ static const char *check_image_kernel(struct swsusp_info *info)
unsigned long snapshot_get_image_size(void)
{
- return nr_copy_pages + nr_meta_pages + 1;
+ return nr_copy_pages + nr_meta_pages + nr_sig_pages + 1;
}
static int init_header(struct swsusp_info *info)
{
+ int error;
+
memset(info, 0, sizeof(struct swsusp_info));
info->num_physpages = get_num_physpages();
info->image_pages = nr_copy_pages;
info->pages = snapshot_get_image_size();
info->size = info->pages;
info->size <<= PAGE_SHIFT;
+ info->sig_pages = nr_sig_pages;
+ if (nr_sig_pages) {
+ error = hib_sig->begin(info->sig_header,
+ sizeof(info->sig_header));
+ if (error)
+ return error;
+ }
return init_header_complete(info);
}
@@ -2234,7 +2279,7 @@ static inline void pack_pfns(unsigned long *buf, struct memory_bitmap *bm,
*/
int snapshot_read_next(struct snapshot_handle *handle)
{
- if (handle->cur > nr_meta_pages + nr_copy_pages)
+ if (handle->cur > nr_meta_pages + nr_copy_pages + nr_sig_pages)
return 0;
if (!buffer) {
@@ -2246,6 +2291,7 @@ int snapshot_read_next(struct snapshot_handle *handle)
if (!handle->cur) {
int error;
+ hib_sig_start();
error = init_header((struct swsusp_info *)buffer);
if (error)
return error;
@@ -2255,6 +2301,19 @@ int snapshot_read_next(struct snapshot_handle *handle)
} else if (handle->cur <= nr_meta_pages) {
clear_page(buffer);
pack_pfns(buffer, &orig_bm, &zero_bm);
+ } else if (handle->cur > nr_meta_pages + nr_copy_pages) {
+ int error;
+
+ /* the signature page, covering every page before it */
+ sha256_final(&hib_sig_ctx, hib_sig_digest);
+ clear_page(buffer);
+ error = hib_sig->sign(hib_sig_digest, buffer,
+ HIB_SIG_TRAILER_SIZE);
+ if (error)
+ return error;
+ handle->buffer = buffer;
+ handle->cur++;
+ return PAGE_SIZE;
} else {
struct page *page;
@@ -2275,6 +2334,8 @@ int snapshot_read_next(struct snapshot_handle *handle)
handle->buffer = page_address(page);
}
}
+ if (nr_sig_pages)
+ sha256_update(&hib_sig_ctx, handle->buffer, PAGE_SIZE);
handle->cur++;
return PAGE_SIZE;
}
@@ -2339,11 +2400,32 @@ static int load_header(struct swsusp_info *info)
restore_pblist = NULL;
error = check_header(info);
- if (!error) {
- nr_copy_pages = info->image_pages;
- nr_meta_pages = info->pages - info->image_pages - 1;
+ if (error)
+ return error;
+
+ if (info->sig_pages > 1)
+ return -EINVAL;
+
+ nr_copy_pages = info->image_pages;
+ nr_sig_pages = info->sig_pages;
+ nr_meta_pages = info->pages - info->image_pages - nr_sig_pages - 1;
+
+ /* without signing enabled, a signature page is ignored */
+ if (!hib_sig)
+ return 0;
+
+ if (!nr_sig_pages) {
+ pr_err("Image is not signed\n");
+ return -EKEYREJECTED;
}
- return error;
+
+ error = hib_sig->check_header(info->sig_header,
+ sizeof(info->sig_header));
+ if (error)
+ return error;
+
+ sha256_update(&hib_sig_ctx, (u8 *)info, PAGE_SIZE);
+ return 0;
}
/**
@@ -2771,10 +2853,38 @@ int snapshot_write_next(struct snapshot_handle *handle)
static struct chain_allocator ca;
int error;
+ if (!handle->cur) {
+ /* A new load: discard any state left by an abandoned one. */
+ hib_sig_start();
+ nr_sig_pages = 0;
+ } else if (hib_sig_hash_pending) {
+ /* The caller has filled the page handed out last time. */
+ hib_sig_hash_pending = false;
+ if (hib_sig_trailer_out)
+ hib_sig_trailer_in = true;
+ else
+ sha256_update(&hib_sig_ctx, handle->buffer, PAGE_SIZE);
+ }
+
next:
/* Check if we have already loaded the entire image */
- if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages)
- return 0;
+ if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages) {
+ if (!nr_sig_pages || hib_sig_trailer_out)
+ return 0;
+
+ /* Hand out the signature page, which is not restored. */
+ copy_last_highmem_page();
+ error = hibernate_restore_protect_page(handle->buffer);
+ if (error)
+ return error;
+ sha256_final(&hib_sig_ctx, hib_sig_digest);
+ handle->buffer = hib_sig_page;
+ handle->sync_read = true;
+ hib_sig_trailer_out = true;
+ hib_sig_hash_pending = true;
+ handle->cur++;
+ return PAGE_SIZE;
+ }
if (!handle->cur) {
if (!buffer)
@@ -2841,6 +2951,8 @@ int snapshot_write_next(struct snapshot_handle *handle)
goto next;
}
+ /* The header is hashed by load_header() once it is known to be signed */
+ hib_sig_hash_pending = handle->cur > 1 && nr_sig_pages && hib_sig;
return PAGE_SIZE;
}
@@ -2868,7 +2980,9 @@ int snapshot_write_finalize(struct snapshot_handle *handle)
return error > 0 ? -ENODATA : error;
}
copy_last_highmem_page();
- error = hibernate_restore_protect_page(handle->buffer);
+ /* The last image page was protected when the signature page was handed out */
+ error = hib_sig_trailer_out ? 0 :
+ hibernate_restore_protect_page(handle->buffer);
/* Do that only if we have loaded the image entirely */
if (handle->cur > 1 && handle->cur > nr_meta_pages + nr_copy_pages + nr_zero_pages) {
memory_bm_recycle(&orig_bm);
@@ -2880,7 +2994,34 @@ int snapshot_write_finalize(struct snapshot_handle *handle)
int snapshot_image_loaded(struct snapshot_handle *handle)
{
return !(!nr_copy_pages || !last_highmem_page_copied() ||
- handle->cur <= nr_meta_pages + nr_copy_pages + nr_zero_pages);
+ handle->cur <= nr_meta_pages + nr_copy_pages + nr_zero_pages ||
+ (nr_sig_pages && !hib_sig_trailer_in));
+}
+
+/**
+ * snapshot_image_signed - Check if the image being loaded is signed.
+ */
+bool snapshot_image_signed(void)
+{
+ return nr_sig_pages && hib_sig;
+}
+
+/**
+ * snapshot_image_verify - Verify the signature of a loaded image.
+ *
+ * Return: 0 if signing is disabled or the image has a valid signature, or
+ * a negative error code otherwise.
+ */
+int snapshot_image_verify(void)
+{
+ if (!hib_sig)
+ return 0;
+
+ if (!nr_sig_pages || !hib_sig_trailer_in)
+ return -EKEYREJECTED;
+
+ return hib_sig->verify(hib_sig_digest, hib_sig_page,
+ HIB_SIG_TRAILER_SIZE);
}
#ifdef CONFIG_HIGHMEM
diff --git a/kernel/power/swap.c b/kernel/power/swap.c
index c78f1593600b..f8840cb1a2a6 100644
--- a/kernel/power/swap.c
+++ b/kernel/power/swap.c
@@ -1116,7 +1116,8 @@ static int load_image(struct swap_map_handle *handle,
ret = swap_read_page(handle, data_of(*snapshot), &hb);
if (ret)
break;
- if (snapshot->sync_read)
+ /* each page must be complete before it is hashed */
+ if (snapshot->sync_read || snapshot_image_signed())
ret = hib_wait_io(&hb);
if (ret)
break;
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 15/17] PM: hibernate: Sign and verify images with a kernel-generated TPM key
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (13 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 14/17] PM: hibernate: Add image digest and signature page infrastructure Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 16/17] PM: hibernate: Refuse to verify images with a virtual TPM Matthew Garrett
` (2 subsequent siblings)
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Add CONFIG_HIBERNATION_TPM_SIGNATURE, a hibernation image signing
backend that uses a TPM signing key created by the kernel.
When an image header is built, a fresh kernel signing key is created
with tpm2_kernel_key_create(), and its provenance, the AK-signed audit
session showing that the key was created while only the kernel could
use the TPM, is stored in the image header. Once the whole image has
been hashed, the digest is signed with the key, and the key is
destroyed. The key cannot be recreated, and as it is created under the
null hierarchy it does not survive a TPM reset, so it can sign nothing
else once the system has been powered off.
When an image is loaded, the provenance is checked with
tpm2_kernel_key_verify() as soon as the header has been read, which
yields the public key that the kernel created. The image is restored
only if its signature verifies with that key. Images that are unsigned,
modified, or signed with any other key are rejected, and the system
boots normally.
As PCR 5 can only distinguish the kernel's audit session if firmware
extends it when ExitBootServices() is called, verification requires that
we successully extend PCR 5 during the EFI stub and that the firmware
then extends it when ExitBootServices is called. Hibernation is
unavailable when the option is enabled but there is no TPM 2.0 or the
firmware does not extend PCR 5.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
kernel/power/Kconfig | 18 +++
kernel/power/Makefile | 1 +
kernel/power/hibernate.c | 4 +-
kernel/power/hibernate_tpm.c | 227 +++++++++++++++++++++++++++++++++++
kernel/power/power.h | 7 +-
kernel/power/snapshot.c | 11 +-
6 files changed, 265 insertions(+), 3 deletions(-)
create mode 100644 kernel/power/hibernate_tpm.c
diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index 846f0f91dbc0..2ff9f807ab5a 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -116,6 +116,24 @@ config HIBERNATION_DEF_COMP
help
Default compressor to be used for hibernation.
+config HIBERNATION_TPM_SIGNATURE
+ bool "Sign hibernation images with a kernel-generated TPM key"
+ depends on HIBERNATION && KERNEL_TPM_SECURITY && TCG_TPM2_KERNEL_KEY
+ help
+ Sign every hibernation image with a TPM key created by the kernel
+ while the image is written, and refuse to restore images that are
+ unsigned, modified, or signed by a key that cannot be shown to have
+ been created by the kernel. This detects images that have been
+ tampered with or written by something other than the kernel, such
+ as userspace.
+
+ The TPM must be a TPM 2.0 whose owner and endorsement hierarchies
+ have empty auth values, and the firmware must extend PCR 5 when
+ ExitBootServices() is called. If these requirements are not met,
+ hibernation is unavailable.
+
+ If unsure, say N.
+
config PM_STD_PARTITION
string "Default resume partition"
depends on HIBERNATION
diff --git a/kernel/power/Makefile b/kernel/power/Makefile
index 773e2789412b..25a4ab12552e 100644
--- a/kernel/power/Makefile
+++ b/kernel/power/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_SUSPEND) += suspend.o
obj-$(CONFIG_PM_TEST_SUSPEND) += suspend_test.o
obj-$(CONFIG_HIBERNATION) += hibernate.o snapshot.o swap.o
obj-$(CONFIG_HIBERNATION_SNAPSHOT_DEV) += user.o
+obj-$(CONFIG_HIBERNATION_TPM_SIGNATURE) += hibernate_tpm.o
obj-$(CONFIG_PM_AUTOSLEEP) += autosleep.o
obj-$(CONFIG_PM_WAKELOCKS) += wakelock.o
diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c
index 1efc2bd3a387..256790aeca86 100644
--- a/kernel/power/hibernate.c
+++ b/kernel/power/hibernate.c
@@ -110,7 +110,9 @@ bool hibernation_available(void)
{
return nohibernate == 0 &&
!security_locked_down(LOCKDOWN_HIBERNATION) &&
- !secretmem_active() && !cxl_mem_active();
+ !secretmem_active() && !cxl_mem_active() &&
+ (!IS_ENABLED(CONFIG_HIBERNATION_TPM_SIGNATURE) ||
+ hibernate_tpm_available());
}
/**
diff --git a/kernel/power/hibernate_tpm.c b/kernel/power/hibernate_tpm.c
new file mode 100644
index 000000000000..96f01c80f3cb
--- /dev/null
+++ b/kernel/power/hibernate_tpm.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Sign hibernation images with a kernel-generated TPM key.
+ *
+ * When an image is created, the kernel creates a fresh TPM signing key,
+ * along with evidence signed by the kernel attestation key that the key
+ * was created by the kernel rather than by userspace (see
+ * drivers/char/tpm/tpm2-kernel-key.c). The evidence is stored in the
+ * image header, and the key signs the digest of the whole image. The key
+ * cannot be recreated and does not survive a TPM reset, so it can sign
+ * nothing else once the system has been powered off.
+ *
+ * On resume, the evidence is checked as soon as the image header has been
+ * read, which yields the public key that the kernel created, and the
+ * image is only restored if it was signed with that key.
+ */
+
+#define pr_fmt(fmt) "PM: hibernation: " fmt
+
+#include <linux/efi.h>
+#include <linux/init.h>
+#include <linux/tpm.h>
+
+#include "power.h"
+
+#define HIB_TPM_SIG_MAGIC "LNXHSIG1"
+
+/* The signature page */
+struct hib_tpm_sig {
+ u8 magic[8];
+ u8 x[EC_PT_SZ];
+ u8 y[EC_PT_SZ];
+ u8 r[EC_PT_SZ];
+ u8 s[EC_PT_SZ];
+};
+
+static_assert(sizeof(struct hib_tpm_sig) <= HIB_SIG_TRAILER_SIZE);
+static_assert(sizeof(struct tpm2_key_provenance) <= HIB_SIG_HEADER_SIZE);
+
+/* The key used to sign the image being created */
+static struct tpm2_kernel_key hib_tpm_key;
+static bool hib_tpm_key_valid;
+
+/* The public key shown to have been created by the kernel, on restore */
+static u8 hib_tpm_x[EC_PT_SZ], hib_tpm_y[EC_PT_SZ];
+static bool hib_tpm_header_ok;
+
+static struct tpm_chip *hib_tpm_get_chip(void)
+{
+ struct tpm_chip *chip = tpm_default_chip();
+
+ if (!chip)
+ return NULL;
+
+ if (!(chip->flags & TPM_CHIP_FLAG_TPM2) || tpm_try_get_ops(chip)) {
+ put_device(&chip->dev);
+ return NULL;
+ }
+
+ return chip;
+}
+
+static void hib_tpm_put_chip(struct tpm_chip *chip)
+{
+ tpm_put_ops(chip);
+ put_device(&chip->dev);
+}
+
+/* TPM errors are positive; report them as a failure to use the TPM */
+static int hib_tpm_err(int rc)
+{
+ return rc > 0 ? -EIO : rc;
+}
+
+static void hib_tpm_end(void)
+{
+ struct tpm_chip *chip;
+
+ if (!hib_tpm_key_valid)
+ return;
+
+ chip = hib_tpm_get_chip();
+ if (chip) {
+ tpm2_kernel_key_destroy(chip, &hib_tpm_key);
+ hib_tpm_put_chip(chip);
+ }
+
+ memzero_explicit(&hib_tpm_key, sizeof(hib_tpm_key));
+ hib_tpm_key_valid = false;
+}
+
+static int hib_tpm_begin(void *data, size_t size)
+{
+ struct tpm2_key_provenance *prov = data;
+ struct tpm_chip *chip;
+ int rc;
+
+ /* the image may be read more than once */
+ hib_tpm_end();
+
+ chip = hib_tpm_get_chip();
+ if (!chip)
+ return -ENODEV;
+
+ rc = tpm2_kernel_key_create(chip, &hib_tpm_key, prov);
+ hib_tpm_put_chip(chip);
+ if (rc) {
+ pr_err("Failed to create image signing key: %d\n", rc);
+ return hib_tpm_err(rc);
+ }
+
+ hib_tpm_key_valid = true;
+ return 0;
+}
+
+static int hib_tpm_sign(const u8 *digest, void *trailer, size_t size)
+{
+ struct hib_tpm_sig *sig = trailer;
+ struct tpm_chip *chip;
+ int rc;
+
+ if (!hib_tpm_key_valid)
+ return -EINVAL;
+
+ chip = hib_tpm_get_chip();
+ if (!chip)
+ return -ENODEV;
+
+ memcpy(sig->magic, HIB_TPM_SIG_MAGIC, sizeof(sig->magic));
+ memcpy(sig->x, hib_tpm_key.x, sizeof(sig->x));
+ memcpy(sig->y, hib_tpm_key.y, sizeof(sig->y));
+ rc = tpm2_kernel_key_sign(chip, &hib_tpm_key, digest, sig->r, sig->s);
+
+ /* the key has done its job */
+ tpm2_kernel_key_destroy(chip, &hib_tpm_key);
+ hib_tpm_key_valid = false;
+ hib_tpm_put_chip(chip);
+
+ if (rc) {
+ pr_err("Failed to sign image: %d\n", rc);
+ return hib_tpm_err(rc);
+ }
+
+ return 0;
+}
+
+static int hib_tpm_check_header(const void *data, size_t size)
+{
+ const struct tpm2_key_provenance *prov = data;
+ struct tpm_chip *chip;
+ int rc;
+
+ hib_tpm_header_ok = false;
+
+ /* PCR 5 can only distinguish the kernel if firmware extends it */
+ if (!kernel_tpm_security_available) {
+ pr_err("Cannot verify image: TPM security not available\n");
+ return -EKEYREJECTED;
+ }
+
+ chip = hib_tpm_get_chip();
+ if (!chip) {
+ pr_err("Cannot verify image: no TPM\n");
+ return -ENODEV;
+ }
+
+ rc = tpm2_kernel_key_verify(chip, prov, hib_tpm_x, hib_tpm_y);
+ hib_tpm_put_chip(chip);
+ if (rc) {
+ pr_err("Image signing key was not created by the kernel: %d\n",
+ rc);
+ return rc > 0 ? -EKEYREJECTED : rc;
+ }
+
+ hib_tpm_header_ok = true;
+ return 0;
+}
+
+static int hib_tpm_verify(const u8 *digest, const void *trailer, size_t size)
+{
+ const struct hib_tpm_sig *sig = trailer;
+
+ if (!hib_tpm_header_ok)
+ return -EKEYREJECTED;
+
+ if (memcmp(sig->magic, HIB_TPM_SIG_MAGIC, sizeof(sig->magic)))
+ return -EKEYREJECTED;
+
+ /* the image must be signed with the key the kernel created */
+ if (memcmp(sig->x, hib_tpm_x, sizeof(hib_tpm_x)) ||
+ memcmp(sig->y, hib_tpm_y, sizeof(hib_tpm_y)))
+ return -EKEYREJECTED;
+
+ return tpm2_kernel_key_verify_signature(hib_tpm_x, hib_tpm_y, digest,
+ sig->r, sig->s);
+}
+
+const struct hib_sig_ops hib_tpm_sig_ops = {
+ .begin = hib_tpm_begin,
+ .sign = hib_tpm_sign,
+ .end = hib_tpm_end,
+ .check_header = hib_tpm_check_header,
+ .verify = hib_tpm_verify,
+};
+
+/**
+ * hibernate_tpm_available - Check whether images can be signed.
+ *
+ * Signing requires a TPM 2.0 and firmware that extends PCR 5 when
+ * ExitBootServices() is called.
+ */
+bool hibernate_tpm_available(void)
+{
+ struct tpm_chip *chip;
+ bool tpm2;
+
+ if (!kernel_tpm_security_available)
+ return false;
+
+ chip = tpm_default_chip();
+ if (!chip)
+ return false;
+
+ tpm2 = chip->flags & TPM_CHIP_FLAG_TPM2;
+ put_device(&chip->dev);
+ return tpm2;
+}
diff --git a/kernel/power/power.h b/kernel/power/power.h
index b4733d45e6cd..8115df3364a9 100644
--- a/kernel/power/power.h
+++ b/kernel/power/power.h
@@ -197,7 +197,12 @@ int snapshot_write_finalize(struct snapshot_handle *handle);
extern int snapshot_image_loaded(struct snapshot_handle *handle);
extern bool snapshot_image_signed(void);
extern int snapshot_image_verify(void);
-extern void hibernate_set_sig_ops(const struct hib_sig_ops *ops);
+#ifdef CONFIG_HIBERNATION_TPM_SIGNATURE
+extern const struct hib_sig_ops hib_tpm_sig_ops;
+extern bool hibernate_tpm_available(void);
+#else
+static inline bool hibernate_tpm_available(void) { return false; }
+#endif
extern bool hibernate_acquire(void);
extern void hibernate_release(void);
diff --git a/kernel/power/snapshot.c b/kernel/power/snapshot.c
index bbf9de4b15ab..75276b73a913 100644
--- a/kernel/power/snapshot.c
+++ b/kernel/power/snapshot.c
@@ -174,7 +174,16 @@ static void *buffer;
* of the image is signed, and the signature is stored in an extra page at
* the end of the image.
*/
-static const struct hib_sig_ops *hib_sig;
+#ifdef CONFIG_HIBERNATION_TPM_SIGNATURE
+/*
+ * Fixed at build time rather than registered at runtime, so that signature
+ * enforcement cannot be avoided by preventing registration, for example
+ * with initcall_blacklist=.
+ */
+static const struct hib_sig_ops *const hib_sig = &hib_tpm_sig_ops;
+#else
+static const struct hib_sig_ops *const hib_sig;
+#endif
static unsigned long nr_sig_pages;
static struct sha256_ctx hib_sig_ctx;
static u8 hib_sig_digest[SHA256_DIGEST_SIZE];
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 16/17] PM: hibernate: Refuse to verify images with a virtual TPM
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (14 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 15/17] PM: hibernate: Sign and verify images with a kernel-generated TPM key Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 13:20 ` [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images Matthew Garrett
2026-10-08 15:56 ` [RFC] Make hibernation work with lockdown Jarkko Sakkinen
17 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Images are verified with whichever TPM tpm_default_chip() returns. We
don't ensure integrity of initramfs, so an attacker can unbind the
hardware TPM's driver and create a virtual TPM through /dev/vtpmx in its
place. A virtual TPM can answer verification however it likes, including
recreating an AK of its own choosing and reporting any PCR 5 value, so
it can be made to accept a forged image.
Refuse to verify images when the TPM is virtual. Add
CONFIG_TPM_HIBERNATE_INSECURE to allow it anyway, for testing, but never
while the kernel is locked down.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
kernel/power/Kconfig | 12 ++++++++++++
kernel/power/hibernate_tpm.c | 14 ++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index 2ff9f807ab5a..2eb6af9226f7 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -134,6 +134,18 @@ config HIBERNATION_TPM_SIGNATURE
If unsure, say N.
+config TPM_HIBERNATE_INSECURE
+ bool "Allow resume using a virtual TPM (INSECURE)"
+ depends on HIBERNATION_TPM_SIGNATURE
+ help
+ By default, hibernation images are not restored if the TPM used
+ to verify them is a virtual TPM, such as one provided by
+ userspace through /dev/vtpmx, since such a TPM can be made to
+ accept a forged image. Say Y to allow it anyway, for example for
+ testing. This has no effect while the kernel is locked down.
+
+ If unsure, say N.
+
config PM_STD_PARTITION
string "Default resume partition"
depends on HIBERNATION
diff --git a/kernel/power/hibernate_tpm.c b/kernel/power/hibernate_tpm.c
index 96f01c80f3cb..43f8d7324360 100644
--- a/kernel/power/hibernate_tpm.c
+++ b/kernel/power/hibernate_tpm.c
@@ -19,6 +19,7 @@
#include <linux/efi.h>
#include <linux/init.h>
+#include <linux/security.h>
#include <linux/tpm.h>
#include "power.h"
@@ -164,6 +165,19 @@ static int hib_tpm_check_header(const void *data, size_t size)
return -ENODEV;
}
+ /*
+ * A virtual TPM, such as one provided by userspace through
+ * /dev/vtpmx, can answer verification with anything it likes. Only
+ * allow one if explicitly configured to, and never under lockdown.
+ */
+ if ((chip->flags & TPM_CHIP_FLAG_VIRTUAL) &&
+ (!IS_ENABLED(CONFIG_TPM_HIBERNATE_INSECURE) ||
+ security_locked_down(LOCKDOWN_HIBERNATION))) {
+ hib_tpm_put_chip(chip);
+ pr_err("Cannot verify image: TPM is virtual\n");
+ return -EKEYREJECTED;
+ }
+
rc = tpm2_kernel_key_verify(chip, prov, hib_tpm_x, hib_tpm_y);
hib_tpm_put_chip(chip);
if (rc) {
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (15 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 16/17] PM: hibernate: Refuse to verify images with a virtual TPM Matthew Garrett
@ 2026-10-08 13:20 ` Matthew Garrett
2026-10-08 16:53 ` Jarkko Sakkinen
2026-10-08 15:56 ` [RFC] Make hibernation work with lockdown Jarkko Sakkinen
17 siblings, 1 reply; 31+ messages in thread
From: Matthew Garrett @ 2026-10-08 13:20 UTC (permalink / raw)
To: mjg59
Cc: keyrings, James.Bottomley, linux-integrity, rafael, linux-pm,
linux-efi, Matthew Garrett
Hibernation is unavailable when the kernel is locked down, as an image
could be modified to alter the running kernel when it is restored. With
CONFIG_HIBERNATION_TPM_SIGNATURE, only images signed with a key that the
kernel created are restored, so this no longer applies.
Allow hibernation under lockdown when images are signed. Images are not
encrypted, though, and contain all of kernel memory, which would then be
readable from the swap device or through /dev/snapshot. Add a
LOCKDOWN_HIBERNATION_IMAGE reason at the confidentiality level, so that
hibernation remains unavailable when the kernel is locked down for
confidentiality.
Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
---
include/linux/security.h | 1 +
kernel/power/Kconfig | 5 +++++
kernel/power/hibernate.c | 18 +++++++++++++-----
security/security.c | 1 +
4 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/include/linux/security.h b/include/linux/security.h
index 153e9043058f..6b4dfe80501b 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -155,6 +155,7 @@ enum lockdown_reason {
LOCKDOWN_TRACEFS,
LOCKDOWN_XMON_RW,
LOCKDOWN_XFRM_SECRET,
+ LOCKDOWN_HIBERNATION_IMAGE,
LOCKDOWN_CONFIDENTIALITY_MAX,
};
diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index 2eb6af9226f7..639dc124c17d 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -132,6 +132,11 @@ config HIBERNATION_TPM_SIGNATURE
ExitBootServices() is called. If these requirements are not met,
hibernation is unavailable.
+ As only signed images are restored, hibernation remains available
+ when the kernel is locked down for integrity. Images are not
+ encrypted, so it is unavailable when the kernel is locked down for
+ confidentiality.
+
If unsure, say N.
config TPM_HIBERNATE_INSECURE
diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c
index 256790aeca86..7bbef9c3ae1c 100644
--- a/kernel/power/hibernate.c
+++ b/kernel/power/hibernate.c
@@ -108,11 +108,19 @@ bool hibernation_in_progress(void)
bool hibernation_available(void)
{
- return nohibernate == 0 &&
- !security_locked_down(LOCKDOWN_HIBERNATION) &&
- !secretmem_active() && !cxl_mem_active() &&
- (!IS_ENABLED(CONFIG_HIBERNATION_TPM_SIGNATURE) ||
- hibernate_tpm_available());
+ if (nohibernate || secretmem_active() || cxl_mem_active())
+ return false;
+
+ /*
+ * Only images signed by the kernel are restored, so hibernation does
+ * not undermine the integrity of a locked down kernel. Images are
+ * not encrypted, though, so they would expose kernel memory.
+ */
+ if (IS_ENABLED(CONFIG_HIBERNATION_TPM_SIGNATURE))
+ return hibernate_tpm_available() &&
+ !security_locked_down(LOCKDOWN_HIBERNATION_IMAGE);
+
+ return !security_locked_down(LOCKDOWN_HIBERNATION);
}
/**
diff --git a/security/security.c b/security/security.c
index 2ee276ab15c5..5d91ca69bbf1 100644
--- a/security/security.c
+++ b/security/security.c
@@ -71,6 +71,7 @@ const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX + 1] = {
[LOCKDOWN_TRACEFS] = "use of tracefs",
[LOCKDOWN_XMON_RW] = "xmon read and write access",
[LOCKDOWN_XFRM_SECRET] = "xfrm SA secret",
+ [LOCKDOWN_HIBERNATION_IMAGE] = "hibernation with signed images",
[LOCKDOWN_CONFIDENTIALITY_MAX] = "confidentiality",
};
--
2.43.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images
2026-10-08 13:20 ` [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images Matthew Garrett
@ 2026-10-08 16:53 ` Jarkko Sakkinen
2026-10-09 8:31 ` Matthew Garrett
0 siblings, 1 reply; 31+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 16:53 UTC (permalink / raw)
To: Matthew Garrett
Cc: mjg59, keyrings, James.Bottomley, linux-integrity, rafael,
linux-pm, linux-efi
On Thu, Oct 08, 2026 at 06:20:33AM -0700, Matthew Garrett wrote:
> Hibernation is unavailable when the kernel is locked down, as an image
> could be modified to alter the running kernel when it is restored. With
> CONFIG_HIBERNATION_TPM_SIGNATURE, only images signed with a key that the
> kernel created are restored, so this no longer applies.
>
> Allow hibernation under lockdown when images are signed. Images are not
> encrypted, though, and contain all of kernel memory, which would then be
> readable from the swap device or through /dev/snapshot. Add a
> LOCKDOWN_HIBERNATION_IMAGE reason at the confidentiality level, so that
> hibernation remains unavailable when the kernel is locked down for
> confidentiality.
>
> Signed-off-by: Matthew Garrett <matthewg@nvidia.com>
I skimmed the rest of the patches but gave up for now given time and
bandwidth, and also having quite loose grip to the changes until I
run them (it's just complex patch set enough).
What kind of test environment you have in high-level, or could you
give some rough guidelines for a test environment?
I'm thinking of setting up something with Buildroot, QEMU and swtpm
perhaps.
> ---
> include/linux/security.h | 1 +
> kernel/power/Kconfig | 5 +++++
> kernel/power/hibernate.c | 18 +++++++++++++-----
> security/security.c | 1 +
> 4 files changed, 20 insertions(+), 5 deletions(-)
>
> diff --git a/include/linux/security.h b/include/linux/security.h
> index 153e9043058f..6b4dfe80501b 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -155,6 +155,7 @@ enum lockdown_reason {
> LOCKDOWN_TRACEFS,
> LOCKDOWN_XMON_RW,
> LOCKDOWN_XFRM_SECRET,
> + LOCKDOWN_HIBERNATION_IMAGE,
> LOCKDOWN_CONFIDENTIALITY_MAX,
> };
>
> diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
> index 2eb6af9226f7..639dc124c17d 100644
> --- a/kernel/power/Kconfig
> +++ b/kernel/power/Kconfig
> @@ -132,6 +132,11 @@ config HIBERNATION_TPM_SIGNATURE
> ExitBootServices() is called. If these requirements are not met,
> hibernation is unavailable.
>
> + As only signed images are restored, hibernation remains available
> + when the kernel is locked down for integrity. Images are not
> + encrypted, so it is unavailable when the kernel is locked down for
> + confidentiality.
> +
> If unsure, say N.
>
> config TPM_HIBERNATE_INSECURE
> diff --git a/kernel/power/hibernate.c b/kernel/power/hibernate.c
> index 256790aeca86..7bbef9c3ae1c 100644
> --- a/kernel/power/hibernate.c
> +++ b/kernel/power/hibernate.c
> @@ -108,11 +108,19 @@ bool hibernation_in_progress(void)
>
> bool hibernation_available(void)
> {
> - return nohibernate == 0 &&
> - !security_locked_down(LOCKDOWN_HIBERNATION) &&
> - !secretmem_active() && !cxl_mem_active() &&
> - (!IS_ENABLED(CONFIG_HIBERNATION_TPM_SIGNATURE) ||
> - hibernate_tpm_available());
> + if (nohibernate || secretmem_active() || cxl_mem_active())
> + return false;
> +
> + /*
> + * Only images signed by the kernel are restored, so hibernation does
> + * not undermine the integrity of a locked down kernel. Images are
> + * not encrypted, though, so they would expose kernel memory.
> + */
> + if (IS_ENABLED(CONFIG_HIBERNATION_TPM_SIGNATURE))
> + return hibernate_tpm_available() &&
> + !security_locked_down(LOCKDOWN_HIBERNATION_IMAGE);
> +
> + return !security_locked_down(LOCKDOWN_HIBERNATION);
> }
>
> /**
> diff --git a/security/security.c b/security/security.c
> index 2ee276ab15c5..5d91ca69bbf1 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -71,6 +71,7 @@ const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX + 1] = {
> [LOCKDOWN_TRACEFS] = "use of tracefs",
> [LOCKDOWN_XMON_RW] = "xmon read and write access",
> [LOCKDOWN_XFRM_SECRET] = "xfrm SA secret",
> + [LOCKDOWN_HIBERNATION_IMAGE] = "hibernation with signed images",
> [LOCKDOWN_CONFIDENTIALITY_MAX] = "confidentiality",
> };
>
> --
> 2.43.0
>
>
Br, Jarkko
^ permalink raw reply [flat|nested] 31+ messages in thread* Re: [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images
2026-10-08 16:53 ` Jarkko Sakkinen
@ 2026-10-09 8:31 ` Matthew Garrett
0 siblings, 0 replies; 31+ messages in thread
From: Matthew Garrett @ 2026-10-09 8:31 UTC (permalink / raw)
To: Jarkko Sakkinen
Cc: Matthew Garrett, keyrings, James.Bottomley, linux-integrity,
rafael, linux-pm, linux-efi
On Thu, Oct 08, 2026 at 07:53:56PM +0300, Jarkko Sakkinen wrote:
> I skimmed the rest of the patches but gave up for now given time and
> bandwidth, and also having quite loose grip to the changes until I
> run them (it's just complex patch set enough).
>
> What kind of test environment you have in high-level, or could you
> give some rough guidelines for a test environment?
>
> I'm thinking of setting up something with Buildroot, QEMU and swtpm
> perhaps.
Testing has been qemu + swtpm, with some scripts to build a toy
initramfs to automatically do the testing. What would be the best way to
share those with you?
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [RFC] Make hibernation work with lockdown
2026-10-08 13:20 [RFC] Make hibernation work with lockdown Matthew Garrett
` (16 preceding siblings ...)
2026-10-08 13:20 ` [PATCH 17/17] PM: hibernate: Allow hibernation under lockdown with signed images Matthew Garrett
@ 2026-10-08 15:56 ` Jarkko Sakkinen
17 siblings, 0 replies; 31+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 15:56 UTC (permalink / raw)
To: Matthew Garrett
Cc: mjg59, keyrings, James.Bottomley, linux-integrity, rafael,
linux-pm, linux-efi
On Thu, Oct 08, 2026 at 06:20:16AM -0700, Matthew Garrett wrote:
> Hibernation writes out the full system state to disk in an unencrypted
> and unauthenticated manner. Resuming from hibernate reads that data and
> throws it directly into RAM, then jumps into it. This is effectively an
> entirely unauthenticated mechanism for putting whatever you want into
> kernel space. This violates the assumptions around lockdown (root can
> write whatever they want to the swap partition and then trigger a
> resume), and as such lockdown blocks hibernate.
>
> This has made many people unhappy.
>
> This patchset seeks to solve this problem. In order for hibernation to
> be trustworthy we need to be able to prove that the image was generated
> by the kernel and not modified after that. This is not an easy task, and
> requires some infrastructural framework. To that end, this patchset does
> the following:
>
> 1) Co-opts a TPM NV index for the kernel's sole use. This is currently a
> placeholder and we should register one explicitly from an appropriate
> range in order to ensure that we don't conflict with userland.
>
> 2) Does something horrifying with PCR 5 in order to prove that a given
> kernel supports (1). We need to extend and cap a PCR before userland is
> running in order to prove that the kernel has support for this feature,
> but since we don't currently cap any PCRs there's nothing stopping
> userland from doing the same and so achieving the same state. The way
> around this is to rely on a feature of PCR 5 - it is extended as a
> result of ExitBootServices being called, and since the boot stub can
> execute code before that happens we can perform the proof extension
> there and then have it implicitly capped by the firmware's extension.
>
> 3) Adds support for audited TPM sessions in the kernel, allowing us to
> generate signed digests of the commands that were executed in that
> session and their results
>
> 4) Adds support for generating a predictable AK that can be used to sign
> digests from those audit sessions
>
> 5) Adds support for generating a TPM signing key in such a session, and
> using the signed digest to prove that the session took place in the
> kernel
>
> 6) Signs the hibernation image with such a key
>
> An old kernel that doesn't implement (1) won't be able to mimic the same
> PCR 5 value. Userland won't be able to mimic the NV index value because
> the kernel will block it. This means that the only way that key could
> have been created is by the kernel, and so we can trust that the image
> was generated by the kernel.
>
> QUESTIONS:
>
> 1) I haven't tried to make the key management generic, since this isn't
> intended to ever be exposed to userland in any way. Should it be
> integrated into the trusted keys layer anyway?
If we don't have a use case, I don't think so...
>
> 2) Filtering TPM commands from userland isn't ideal - anyone able to
> poke commands into the TPM directly is in a position to violate the
> assumptions here. Is there any way we can get a secret into the kernel
> that can be used as an auth value? It would need to be impossible to
> obtain from userland and it would need to be consistent over platform
> reboots.
We could generate a primary key and a keyedhash key that would be stored
outside kernel while data is at rest. Then during power on they could be
read to the kernel memory.
I did not consider who would create the files in the first place.
I neither did look at the code yet meaning that my comments should
definitely be taken with a grain of salt. Just writing down a remark.
Br, Jarkko
^ permalink raw reply [flat|nested] 31+ messages in thread