[PATCH 7/9] pm: hibernate: Optionally use TPM-backed keys to protect image integrity

From: Matthew Garrett
Date: Fri Feb 19 2021 - 20:36:11 EST


A plain hash protects the hibernation image against accidental
modification, but in the face of an active attack the hash can simply be
updated to match the new image. Generate a random AES key and seal this
with the TPM, and use it to encrypt the hash. On resume, the key can be
unsealed and used to decrypt the hash. By setting PCR 23 to a specific
value we can verify that the key used was generated by the kernel during
hibernation and prevent an attacker providing their own key.

Signed-off-by: Matthew Garrett <mjg59@xxxxxxxxxx>
---
kernel/power/Kconfig | 15 ++
kernel/power/Makefile | 1 +
kernel/power/hibernate.c | 11 +-
kernel/power/swap.c | 99 +++----------
kernel/power/swap.h | 38 +++++
kernel/power/tpm.c | 294 +++++++++++++++++++++++++++++++++++++++
kernel/power/tpm.h | 37 +++++
7 files changed, 417 insertions(+), 78 deletions(-)
create mode 100644 kernel/power/swap.h
create mode 100644 kernel/power/tpm.c
create mode 100644 kernel/power/tpm.h

diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig
index a7320f07689d..0279cc10f319 100644
--- a/kernel/power/Kconfig
+++ b/kernel/power/Kconfig
@@ -92,6 +92,21 @@ config HIBERNATION_SNAPSHOT_DEV

If in doubt, say Y.

+config SECURE_HIBERNATION
+ bool "Implement secure hibernation support"
+ depends on HIBERNATION && TCG_TPM
+ select KEYS
+ select TRUSTED_KEYS
+ select CRYPTO
+ select CRYPTO_SHA256
+ select CRYPTO_AES
+ select TCG_TPM_RESTRICT_PCR
+ help
+ Use a TPM-backed key to securely determine whether a hibernation
+ image was written out by the kernel and has not been tampered with.
+ This requires a TCG-compliant TPM2 device, which is present on most
+ modern hardware.
+
config PM_STD_PARTITION
string "Default resume partition"
depends on HIBERNATION
diff --git a/kernel/power/Makefile b/kernel/power/Makefile
index 5899260a8bef..2edfef897607 100644
--- a/kernel/power/Makefile
+++ b/kernel/power/Makefile
@@ -12,6 +12,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_SECURE_HIBERNATION) += 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 da0b41914177..608bfbee38f5 100644
--- a/kernel/power/hibernate.c
+++ b/kernel/power/hibernate.c
@@ -34,6 +34,7 @@
#include <trace/events/power.h>

#include "power.h"
+#include "tpm.h"


static int nocompress;
@@ -81,7 +82,11 @@ void hibernate_release(void)

bool hibernation_available(void)
{
- return nohibernate == 0 && !security_locked_down(LOCKDOWN_HIBERNATION);
+ if (security_locked_down(LOCKDOWN_HIBERNATION) &&
+ !secure_hibernation_available())
+ return false;
+
+ return nohibernate == 0;
}

/**
@@ -752,7 +757,9 @@ int hibernate(void)
flags |= SF_NOCOMPRESS_MODE;
else
flags |= SF_CRC32_MODE;
-
+#ifdef CONFIG_SECURE_HIBERNATION
+ flags |= SF_VERIFY_IMAGE;
+#endif
pm_pr_dbg("Writing hibernation image.\n");
error = swsusp_write(flags);
swsusp_free();
diff --git a/kernel/power/swap.c b/kernel/power/swap.c
index a13241a20567..eaa585731314 100644
--- a/kernel/power/swap.c
+++ b/kernel/power/swap.c
@@ -32,9 +32,10 @@
#include <linux/crc32.h>
#include <linux/ktime.h>
#include <crypto/hash.h>
-#include <crypto/sha2.h>

#include "power.h"
+#include "swap.h"
+#include "tpm.h"

#define HIBERNATE_SIG "S1SUSPEND"

@@ -89,34 +90,6 @@ struct swap_map_page_list {
struct swap_map_page_list *next;
};

-/**
- * The swap_map_handle structure is used for handling swap in
- * a file-alike way
- */
-
-struct swap_map_handle {
- struct swap_map_page *cur;
- struct swap_map_page_list *maps;
- struct shash_desc *desc;
- sector_t cur_swap;
- sector_t first_sector;
- unsigned int k;
- unsigned long reqd_free_pages;
- u32 crc32;
- u8 digest[SHA256_DIGEST_SIZE];
-};
-
-struct swsusp_header {
- char reserved[PAGE_SIZE - 20 - sizeof(sector_t) - sizeof(int) -
- sizeof(u32) - SHA256_DIGEST_SIZE];
- u32 crc32;
- u8 digest[SHA256_DIGEST_SIZE];
- sector_t image;
- unsigned int flags; /* Flags to pass to the "boot" kernel */
- char orig_sig[10];
- char sig[10];
-} __packed;
-
static struct swsusp_header *swsusp_header;

/**
@@ -337,6 +310,9 @@ static int mark_swapfiles(struct swap_map_handle *handle, unsigned int flags)
swsusp_header->crc32 = handle->crc32;
memcpy(swsusp_header->digest, handle->digest,
SHA256_DIGEST_SIZE);
+ error = swsusp_encrypt_digest(swsusp_header);
+ if (error)
+ return error;
error = hib_submit_io(REQ_OP_WRITE, REQ_SYNC,
swsusp_resume_block, swsusp_header, NULL);
} else {
@@ -427,7 +403,6 @@ static void release_swap_writer(struct swap_map_handle *handle)
static int get_swap_writer(struct swap_map_handle *handle)
{
int ret;
- struct crypto_shash *tfm;

ret = swsusp_swap_check();
if (ret) {
@@ -449,27 +424,11 @@ static int get_swap_writer(struct swap_map_handle *handle)
handle->reqd_free_pages = reqd_free_pages();
handle->first_sector = handle->cur_swap;

- tfm = crypto_alloc_shash("sha256", 0, 0);
- if (IS_ERR(tfm)) {
- ret = -EINVAL;
- goto err_rel;
- }
- handle->desc = kmalloc(sizeof(struct shash_desc) +
- crypto_shash_descsize(tfm), GFP_KERNEL);
- if (!handle->desc) {
- ret = -ENOMEM;
+ ret = swsusp_digest_setup(handle);
+ if (ret)
goto err_rel;
- }
-
- handle->desc->tfm = tfm;
-
- ret = crypto_shash_init(handle->desc);
- if (ret != 0)
- goto err_free;

return 0;
-err_free:
- kfree(handle->desc);
err_rel:
release_swap_writer(handle);
err_close:
@@ -486,7 +445,7 @@ static int swap_write_page(struct swap_map_handle *handle, void *buf,
if (!handle->cur)
return -EINVAL;
offset = alloc_swapdev_block(root_swap);
- crypto_shash_update(handle->desc, buf, PAGE_SIZE);
+ swsusp_digest_update(handle, buf, PAGE_SIZE);
error = write_page(buf, offset, hb);
if (error)
return error;
@@ -529,7 +488,7 @@ static int flush_swap_writer(struct swap_map_handle *handle)
static int swap_writer_finish(struct swap_map_handle *handle,
unsigned int flags, int error)
{
- crypto_shash_final(handle->desc, handle->digest);
+ swsusp_digest_final(handle);
if (!error) {
pr_info("S");
error = mark_swapfiles(handle, flags);
@@ -1008,7 +967,6 @@ static int get_swap_reader(struct swap_map_handle *handle,
int error;
struct swap_map_page_list *tmp, *last;
sector_t offset;
- struct crypto_shash *tfm;

*flags_p = swsusp_header->flags;

@@ -1047,27 +1005,12 @@ static int get_swap_reader(struct swap_map_handle *handle,
handle->k = 0;
handle->cur = handle->maps->map;

- tfm = crypto_alloc_shash("sha256", 0, 0);
- if (IS_ERR(tfm)) {
- error = -EINVAL;
- goto err_rel;
- }
- handle->desc = kmalloc(sizeof(struct shash_desc) +
- crypto_shash_descsize(tfm), GFP_KERNEL);
- if (!handle->desc) {
- error = -ENOMEM;
- goto err_rel;
- }
-
- handle->desc->tfm = tfm;
+ error = swsusp_digest_setup(handle);
+ if (error)
+ goto err;

- error = crypto_shash_init(handle->desc);
- if (error != 0)
- goto err_free;
return 0;
-err_free:
- kfree(handle->desc);
-err_rel:
+err:
release_swap_reader(handle);
return error;
}
@@ -1087,7 +1030,7 @@ static int swap_read_page(struct swap_map_handle *handle, void *buf,
error = hib_submit_io(REQ_OP_READ, 0, offset, buf, hb);
if (error)
return error;
- crypto_shash_update(handle->desc, buf, PAGE_SIZE);
+ swsusp_digest_update(handle, buf, PAGE_SIZE);
if (++handle->k >= MAP_PAGE_ENTRIES) {
handle->k = 0;
free_page((unsigned long)handle->maps->map);
@@ -1107,11 +1050,13 @@ static int swap_reader_finish(struct swap_map_handle *handle,
{
int ret = 0;

- crypto_shash_final(handle->desc, handle->digest);
- if (memcmp(handle->digest, swsusp_header->digest,
- SHA256_DIGEST_SIZE) != 0) {
- pr_err("Image digest doesn't match header digest\n");
- ret = -ENODATA;
+ swsusp_digest_final(handle);
+ if (swsusp_header->flags & SF_VERIFY_IMAGE) {
+ if (memcmp(handle->digest, swsusp_header->digest,
+ SHA256_DIGEST_SIZE) != 0) {
+ pr_err("Image digest doesn't match header digest\n");
+ ret = -ENODATA;
+ }
}

release_swap_reader(handle);
@@ -1630,6 +1575,8 @@ int swsusp_check(void)
error = -EINVAL;
}

+ if (!error)
+ error = swsusp_decrypt_digest(swsusp_header);
put:
if (error)
blkdev_put(hib_resume_bdev, FMODE_READ);
diff --git a/kernel/power/swap.h b/kernel/power/swap.h
new file mode 100644
index 000000000000..342189344f5f
--- /dev/null
+++ b/kernel/power/swap.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <keys/trusted-type.h>
+#include <crypto/sha2.h>
+
+#ifndef _POWER_SWAP_H
+#define _POWER_SWAP_H 1
+/**
+ * The swap_map_handle structure is used for handling swap in
+ * a file-alike way
+ */
+
+struct swap_map_handle {
+ struct swap_map_page *cur;
+ struct swap_map_page_list *maps;
+ struct shash_desc *desc;
+ sector_t cur_swap;
+ sector_t first_sector;
+ unsigned int k;
+ unsigned long reqd_free_pages;
+ u32 crc32;
+ u8 digest[SHA256_DIGEST_SIZE];
+};
+
+struct swsusp_header {
+ char reserved[PAGE_SIZE - 20 - sizeof(sector_t) - sizeof(int) -
+ sizeof(u32) - SHA256_DIGEST_SIZE - MAX_BLOB_SIZE -
+ sizeof(u32)];
+ u32 blob_len;
+ u8 blob[MAX_BLOB_SIZE];
+ u8 digest[SHA256_DIGEST_SIZE];
+ u32 crc32;
+ sector_t image;
+ unsigned int flags; /* Flags to pass to the "boot" kernel */
+ char orig_sig[10];
+ char sig[10];
+} __packed;
+
+#endif /* _POWER_SWAP_H */
diff --git a/kernel/power/tpm.c b/kernel/power/tpm.c
new file mode 100644
index 000000000000..953dcbdc56d8
--- /dev/null
+++ b/kernel/power/tpm.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <crypto/hash.h>
+#include <crypto/skcipher.h>
+#include <linux/key-type.h>
+#include <linux/scatterlist.h>
+
+#include "swap.h"
+#include "tpm.h"
+
+/* sha256("To sleep, perchance to dream") */
+static struct tpm_digest digest = { .alg_id = TPM_ALG_SHA256,
+ .digest = {0x92, 0x78, 0x3d, 0x79, 0x2d, 0x00, 0x31, 0xb0, 0x55, 0xf9,
+ 0x1e, 0x0d, 0xce, 0x83, 0xde, 0x1d, 0xc4, 0xc5, 0x8e, 0x8c,
+ 0xf1, 0x22, 0x38, 0x6c, 0x33, 0xb1, 0x14, 0xb7, 0xec, 0x05,
+ 0x5f, 0x49}};
+
+struct skcipher_def {
+ struct scatterlist sg;
+ struct crypto_skcipher *tfm;
+ struct skcipher_request *req;
+ struct crypto_wait wait;
+};
+
+static int swsusp_enc_dec(struct trusted_key_payload *payload, char *buf,
+ int enc)
+{
+ struct skcipher_def sk;
+ struct crypto_skcipher *skcipher = NULL;
+ struct skcipher_request *req = NULL;
+ char *ivdata = NULL;
+ int ret;
+
+ skcipher = crypto_alloc_skcipher("cbc-aes-aesni", 0, 0);
+ if (IS_ERR(skcipher))
+ return PTR_ERR(skcipher);
+
+ req = skcipher_request_alloc(skcipher, GFP_KERNEL);
+ if (!req) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ skcipher_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
+ crypto_req_done,
+ &sk.wait);
+
+ /* AES 256 */
+ if (crypto_skcipher_setkey(skcipher, payload->key, 32)) {
+ ret = -EAGAIN;
+ goto out;
+ }
+
+ /* Key will never be re-used, just fix the IV to 0 */
+ ivdata = kzalloc(16, GFP_KERNEL);
+ if (!ivdata) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ sk.tfm = skcipher;
+ sk.req = req;
+
+ sg_init_one(&sk.sg, buf, 32);
+ skcipher_request_set_crypt(req, &sk.sg, &sk.sg, 16, ivdata);
+ crypto_init_wait(&sk.wait);
+
+ /* perform the operation */
+ if (enc)
+ ret = crypto_wait_req(crypto_skcipher_encrypt(sk.req),
+ &sk.wait);
+ else
+ ret = crypto_wait_req(crypto_skcipher_decrypt(sk.req),
+ &sk.wait);
+
+ if (ret)
+ pr_info("skcipher encrypt returned with result %d\n", ret);
+
+ goto out;
+
+out:
+ if (skcipher)
+ crypto_free_skcipher(skcipher);
+ if (req)
+ skcipher_request_free(req);
+ kfree(ivdata);
+ return ret;
+}
+
+int swsusp_encrypt_digest(struct swsusp_header *header)
+{
+ const struct cred *cred = current_cred();
+ struct trusted_key_payload *payload;
+ struct tpm_digest *digests = NULL;
+ struct tpm_chip *chip;
+ struct key *key;
+ int ret, i;
+
+ char *keyinfo = "new\t32\tkeyhandle=0x81000001";
+
+ chip = tpm_default_chip();
+
+ if (!chip)
+ return -ENODEV;
+
+ if (!(tpm_is_tpm2(chip)))
+ return -ENODEV;
+
+ ret = tpm_pcr_reset(chip, 23);
+ if (ret != 0)
+ return ret;
+
+ digests = kcalloc(chip->nr_allocated_banks, sizeof(struct tpm_digest),
+ GFP_KERNEL);
+ if (!digests) {
+ ret = -ENOMEM;
+ goto reset;
+ }
+
+ for (i = 0; i <= chip->nr_allocated_banks; i++) {
+ digests[i].alg_id = chip->allocated_banks[i].alg_id;
+ if (digests[i].alg_id == digest.alg_id)
+ memcpy(&digests[i], &digest, sizeof(digest));
+ }
+
+ ret = tpm_pcr_extend(chip, 23, digests);
+ if (ret != 0)
+ goto reset;
+
+ key = key_alloc(&key_type_trusted, "swsusp", GLOBAL_ROOT_UID,
+ GLOBAL_ROOT_GID, cred, 0, KEY_ALLOC_NOT_IN_QUOTA,
+ NULL);
+
+ if (IS_ERR(key)) {
+ ret = PTR_ERR(key);
+ goto reset;
+ }
+
+ ret = key_instantiate_and_link(key, keyinfo, strlen(keyinfo) + 1, NULL,
+ NULL);
+ if (ret < 0)
+ goto error;
+
+ payload = key->payload.data[0];
+
+ ret = swsusp_enc_dec(payload, header->digest, 1);
+ if (ret)
+ goto error;
+
+ memcpy(header->blob, payload->blob, payload->blob_len);
+ header->blob_len = payload->blob_len;
+
+error:
+ key_revoke(key);
+ key_put(key);
+reset:
+ kfree(digests);
+ tpm_pcr_reset(chip, 23);
+ return ret;
+}
+
+int swsusp_decrypt_digest(struct swsusp_header *header)
+{
+ const struct cred *cred = current_cred();
+ char *keytemplate = "load\t%s\tkeyhandle=0x81000001";
+ struct trusted_key_payload *payload;
+ struct tpm_digest *digests = NULL;
+ char *blobstring = NULL;
+ char *keyinfo = NULL;
+ struct tpm_chip *chip;
+ struct key *key;
+ int i, ret;
+
+ chip = tpm_default_chip();
+
+ if (!chip)
+ return -ENODEV;
+
+ if (!(tpm_is_tpm2(chip)))
+ return -ENODEV;
+
+ ret = tpm_pcr_reset(chip, 23);
+ if (ret != 0)
+ return ret;
+
+ digests = kcalloc(chip->nr_allocated_banks, sizeof(struct tpm_digest),
+ GFP_KERNEL);
+ if (!digests)
+ goto reset;
+
+ for (i = 0; i <= chip->nr_allocated_banks; i++) {
+ digests[i].alg_id = chip->allocated_banks[i].alg_id;
+ if (digests[i].alg_id == digest.alg_id)
+ memcpy(&digests[i], &digest, sizeof(digest));
+ }
+
+ ret = tpm_pcr_extend(chip, 23, digests);
+ if (ret != 0)
+ goto reset;
+
+ blobstring = kmalloc(header->blob_len * 2, GFP_KERNEL);
+ if (!blobstring) {
+ ret = -ENOMEM;
+ goto reset;
+ }
+
+ bin2hex(blobstring, header->blob, header->blob_len);
+
+ keyinfo = kasprintf(GFP_KERNEL, keytemplate, blobstring);
+ if (!keyinfo) {
+ ret = -ENOMEM;
+ goto reset;
+ }
+
+ key = key_alloc(&key_type_trusted, "swsusp", GLOBAL_ROOT_UID,
+ GLOBAL_ROOT_GID, cred, 0, KEY_ALLOC_NOT_IN_QUOTA,
+ NULL);
+
+ if (IS_ERR(key)) {
+ ret = PTR_ERR(key);
+ goto out;
+ }
+
+ ret = key_instantiate_and_link(key, keyinfo, strlen(keyinfo) + 1, NULL,
+ NULL);
+ if (ret < 0)
+ goto out;
+
+ payload = key->payload.data[0];
+
+ ret = swsusp_enc_dec(payload, header->digest, 0);
+
+out:
+ key_revoke(key);
+ key_put(key);
+reset:
+ kfree(keyinfo);
+ kfree(blobstring);
+ kfree(digests);
+ tpm_pcr_reset(chip, 23);
+ return ret;
+}
+
+int swsusp_digest_setup(struct swap_map_handle *handle)
+{
+ struct crypto_shash *tfm;
+ int ret;
+
+ tfm = crypto_alloc_shash("sha256", 0, 0);
+ if (IS_ERR(tfm))
+ return PTR_ERR(tfm);
+
+ handle->desc = kmalloc(sizeof(struct shash_desc) +
+ crypto_shash_descsize(tfm), GFP_KERNEL);
+ if (!handle->desc) {
+ crypto_free_shash(tfm);
+ return -ENOMEM;
+ }
+
+ handle->desc->tfm = tfm;
+ ret = crypto_shash_init(handle->desc);
+ if (ret != 0) {
+ crypto_free_shash(tfm);
+ kfree(handle->desc);
+ return ret;
+ }
+
+ return 0;
+}
+
+void swsusp_digest_update(struct swap_map_handle *handle, char *buf,
+ size_t size)
+{
+ crypto_shash_update(handle->desc, buf, size);
+}
+
+void swsusp_digest_final(struct swap_map_handle *handle)
+{
+ crypto_shash_final(handle->desc, handle->digest);
+ crypto_free_shash(handle->desc->tfm);
+ kfree(handle->desc);
+}
+
+int secure_hibernation_available(void)
+{
+ struct tpm_chip *chip = tpm_default_chip();
+
+ if (!chip)
+ return -ENODEV;
+
+ if (!(tpm_is_tpm2(chip)))
+ return -ENODEV;
+
+ return 0;
+}
diff --git a/kernel/power/tpm.h b/kernel/power/tpm.h
new file mode 100644
index 000000000000..75b9140e5dc2
--- /dev/null
+++ b/kernel/power/tpm.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include "swap.h"
+
+#ifndef _POWER_TPM_H
+#define _POWER_TPM_H
+
+#ifdef CONFIG_SECURE_HIBERNATION
+int secure_hibernation_available(void);
+int swsusp_encrypt_digest(struct swsusp_header *header);
+int swsusp_decrypt_digest(struct swsusp_header *header);
+int swsusp_digest_setup(struct swap_map_handle *handle);
+void swsusp_digest_update(struct swap_map_handle *handle, char *buf,
+ size_t size);
+void swsusp_digest_final(struct swap_map_handle *handle);
+#else
+static inline int secure_hibernation_available(void)
+{
+ return -ENODEV;
+};
+static inline int swsusp_encrypt_digest(struct swsusp_header *header)
+{
+ return 0;
+}
+static inline int swsusp_decrypt_digest(struct swsusp_header *header)
+{
+ return 0;
+}
+static inline int swsusp_digest_setup(struct swap_map_handle *handle)
+{
+ return 0;
+}
+static inline void swsusp_digest_update(struct swap_map_handle *handle,
+ char *buf, size_t size) {};
+static inline void swsusp_digest_final(struct swap_map_handle *handle) {};
+#endif
+
+#endif /* _POWER_TPM_H */
--
2.30.0.617.g56c4b15f3c-goog