[PATCH v2 09/12] fs-verity: support builtin file signatures

From: Eric Biggers
Date: Thu Nov 01 2018 - 18:54:44 EST


From: Eric Biggers <ebiggers@xxxxxxxxxx>

For ease of use, add optional support for having fs-verity handle a
portion of the authentication policy in the kernel. A ".fs-verity"
keyring is created to which trusted X.509 certificates can be added;
then a sysctl 'fs.verity.require_signatures' can be set to cause the
kernel to enforce that all fs-verity files contain a signature of their
file measurement, signed by a key in this keyring.

See Documentation/filesystem/fsverity.rst for more information,
namely the "Built-in file signatures" section.

Signed-off-by: Eric Biggers <ebiggers@xxxxxxxxxx>
---
fs/verity/Kconfig | 17 ++++
fs/verity/Makefile | 2 +
fs/verity/fsverity_private.h | 34 +++++++
fs/verity/setup.c | 63 +++++++++++-
fs/verity/signature.c | 187 ++++++++++++++++++++++++++++++++++
include/uapi/linux/fsverity.h | 10 ++
6 files changed, 311 insertions(+), 2 deletions(-)
create mode 100644 fs/verity/signature.c

diff --git a/fs/verity/Kconfig b/fs/verity/Kconfig
index 102c46ebe275f..a7470a2e4892f 100644
--- a/fs/verity/Kconfig
+++ b/fs/verity/Kconfig
@@ -33,3 +33,20 @@ config FS_VERITY_DEBUG
Enable debugging messages related to fs-verity by default.

Say N unless you are an fs-verity developer.
+
+config FS_VERITY_BUILTIN_SIGNATURES
+ bool "FS Verity builtin signature support"
+ depends on FS_VERITY
+ select SYSTEM_DATA_VERIFICATION
+ help
+ Support verifying signatures of verity files against the X.509
+ certificates that have been loaded into the ".fs-verity"
+ kernel keyring.
+
+ This is meant as a relatively simple mechanism that can be
+ used to provide an authenticity guarantee for verity files, as
+ an alternative to IMA appraisal. Userspace programs still
+ need to check that the verity bit is set in order to get an
+ authenticity guarantee.
+
+ If unsure, say N.
diff --git a/fs/verity/Makefile b/fs/verity/Makefile
index 6450925e3a8b7..d293ea2a1b393 100644
--- a/fs/verity/Makefile
+++ b/fs/verity/Makefile
@@ -1,3 +1,5 @@
obj-$(CONFIG_FS_VERITY) += fsverity.o

fsverity-y := hash_algs.o ioctl.o setup.o verify.o
+
+fsverity-$(CONFIG_FS_VERITY_BUILTIN_SIGNATURES) += signature.o
diff --git a/fs/verity/fsverity_private.h b/fs/verity/fsverity_private.h
index c3a261a598557..4b39d0a5544ba 100644
--- a/fs/verity/fsverity_private.h
+++ b/fs/verity/fsverity_private.h
@@ -63,6 +63,7 @@ struct fsverity_info {
u8 root_hash[FS_VERITY_MAX_DIGEST_SIZE]; /* Merkle tree root hash */
u8 measurement[FS_VERITY_MAX_DIGEST_SIZE]; /* file measurement */
bool have_root_hash; /* have root hash from disk? */
+ bool have_signed_measurement; /* have measurement from signature? */

/* Starting blocks for each tree level. 'depth-1' is the root level. */
u64 hash_lvl_region_idx[FS_VERITY_MAX_LEVELS];
@@ -95,6 +96,39 @@ static inline bool set_fsverity_info(struct inode *inode,
return cmpxchg_release(&inode->i_verity_info, NULL, vi) == NULL;
}

+/* signature.c */
+#ifdef CONFIG_FS_VERITY_BUILTIN_SIGNATURES
+extern int fsverity_require_signatures;
+
+int fsverity_parse_pkcs7_signature_extension(struct fsverity_info *vi,
+ const void *raw_pkcs7,
+ size_t size);
+
+int __init fsverity_signature_init(void);
+
+void __exit fsverity_signature_exit(void);
+#else /* CONFIG_FS_VERITY_BUILTIN_SIGNATURES */
+
+#define fsverity_require_signatures 0
+
+static inline int
+fsverity_parse_pkcs7_signature_extension(struct fsverity_info *vi,
+ const void *raw_pkcs7, size_t size)
+{
+ pr_warn("PKCS#7 signatures not supported in this kernel build!\n");
+ return -EINVAL;
+}
+
+static inline int fsverity_signature_init(void)
+{
+ return 0;
+}
+
+static inline void fsverity_signature_exit(void)
+{
+}
+#endif /* !CONFIG_FS_VERITY_BUILTIN_SIGNATURES */
+
/* verify.c */
extern struct workqueue_struct *fsverity_read_workqueue;

diff --git a/fs/verity/setup.c b/fs/verity/setup.c
index e0b39c518b890..08b609127531b 100644
--- a/fs/verity/setup.c
+++ b/fs/verity/setup.c
@@ -132,6 +132,10 @@ static const struct extension_type {
[FS_VERITY_EXT_SALT] = {
.parse = parse_salt_extension,
},
+ [FS_VERITY_EXT_PKCS7_SIGNATURE] = {
+ .parse = fsverity_parse_pkcs7_signature_extension,
+ .unauthenticated = true,
+ },
};

static int do_parse_extensions(struct fsverity_info *vi,
@@ -429,6 +433,54 @@ static int compute_measurement(const struct fsverity_info *vi,
return err;
}

+/*
+ * Compute the file's measurement; then, if a signature was present, verify that
+ * the signed measurement matches the actual one.
+ */
+static int
+verify_file_measurement(struct fsverity_info *vi,
+ const struct fsverity_descriptor *desc,
+ int desc_auth_len,
+ struct page *desc_pages[MAX_DESCRIPTOR_PAGES],
+ int nr_desc_pages)
+{
+ u8 measurement[FS_VERITY_MAX_DIGEST_SIZE];
+ int err;
+
+ err = compute_measurement(vi, desc, desc_auth_len, desc_pages,
+ nr_desc_pages, measurement);
+ if (err) {
+ pr_warn("Error computing fs-verity measurement: %d\n", err);
+ return err;
+ }
+
+ if (!vi->have_signed_measurement) {
+ pr_debug("Computed measurement: %s:%*phN (used desc_auth_len %d)\n",
+ vi->hash_alg->name, vi->hash_alg->digest_size,
+ measurement, desc_auth_len);
+ if (fsverity_require_signatures) {
+ pr_warn("require_signatures=1, rejecting unsigned file!\n");
+ return -EBADMSG;
+ }
+ memcpy(vi->measurement, measurement, vi->hash_alg->digest_size);
+ return 0;
+ }
+
+ if (!memcmp(measurement, vi->measurement, vi->hash_alg->digest_size)) {
+ pr_debug("Verified measurement: %s:%*phN (used desc_auth_len %d)\n",
+ vi->hash_alg->name, vi->hash_alg->digest_size,
+ measurement, desc_auth_len);
+ return 0;
+ }
+
+ pr_warn("FILE CORRUPTED (actual measurement mismatches signed measurement): "
+ "want %s:%*phN, real %s:%*phN (used desc_auth_len %d)\n",
+ vi->hash_alg->name, vi->hash_alg->digest_size, vi->measurement,
+ vi->hash_alg->name, vi->hash_alg->digest_size, measurement,
+ desc_auth_len);
+ return -EBADMSG;
+}
+
static struct fsverity_info *alloc_fsverity_info(void)
{
return kmem_cache_zalloc(fsverity_info_cachep, GFP_NOFS);
@@ -674,8 +726,8 @@ struct fsverity_info *create_fsverity_info(struct inode *inode, bool enabling)
err = compute_tree_depth_and_offsets(vi);
if (err)
goto out;
- err = compute_measurement(vi, desc, desc_auth_len, desc_pages,
- nr_desc_pages, vi->measurement);
+ err = verify_file_measurement(vi, desc, desc_auth_len,
+ desc_pages, nr_desc_pages);
out:
if (desc)
unmap_fsverity_descriptor(desc, desc_pages, nr_desc_pages);
@@ -825,11 +877,17 @@ static int __init fsverity_module_init(void)
if (!fsverity_info_cachep)
goto error_free_workqueue;

+ err = fsverity_signature_init();
+ if (err)
+ goto error_free_info_cache;
+
fsverity_check_hash_algs();

pr_debug("Initialized fs-verity\n");
return 0;

+error_free_info_cache:
+ kmem_cache_destroy(fsverity_info_cachep);
error_free_workqueue:
destroy_workqueue(fsverity_read_workqueue);
error:
@@ -840,6 +898,7 @@ static void __exit fsverity_module_exit(void)
{
destroy_workqueue(fsverity_read_workqueue);
kmem_cache_destroy(fsverity_info_cachep);
+ fsverity_signature_exit();
fsverity_exit_hash_algs();
}

diff --git a/fs/verity/signature.c b/fs/verity/signature.c
new file mode 100644
index 0000000000000..e13b25becbc6f
--- /dev/null
+++ b/fs/verity/signature.c
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * fs/verity/signature.c: verification of builtin signatures
+ *
+ * Copyright 2018 Google LLC
+ *
+ * Written by Eric Biggers.
+ */
+
+#include "fsverity_private.h"
+
+#include <linux/cred.h>
+#include <linux/key.h>
+#include <linux/verification.h>
+
+/*
+ * /proc/sys/fs/verity/require_signatures
+ * If 1, all verity files must have a valid builtin signature.
+ */
+int fsverity_require_signatures;
+
+/*
+ * Keyring that contains the trusted X.509 certificates.
+ *
+ * Only root (kuid=0) can modify this. Also, root may use
+ * keyctl_restrict_keyring() to prevent any more additions.
+ */
+static struct key *fsverity_keyring;
+
+static int extract_measurement(void *ctx, const void *data, size_t len,
+ size_t asn1hdrlen)
+{
+ struct fsverity_info *vi = ctx;
+ const struct fsverity_digest_disk *d;
+ const struct fsverity_hash_alg *hash_alg;
+
+ if (len < sizeof(*d)) {
+ pr_warn("Signed file measurement has unrecognized format\n");
+ return -EBADMSG;
+ }
+ d = (const void *)data;
+
+ hash_alg = fsverity_get_hash_alg(le16_to_cpu(d->digest_algorithm));
+ if (IS_ERR(hash_alg))
+ return PTR_ERR(hash_alg);
+
+ if (le16_to_cpu(d->digest_size) != hash_alg->digest_size) {
+ pr_warn("Wrong digest_size in signed measurement: wanted %u for algorithm %s, but got %u\n",
+ hash_alg->digest_size, hash_alg->name,
+ le16_to_cpu(d->digest_size));
+ return -EBADMSG;
+ }
+
+ if (len < sizeof(*d) + hash_alg->digest_size) {
+ pr_warn("Signed file measurement is truncated\n");
+ return -EBADMSG;
+ }
+
+ if (hash_alg != vi->hash_alg) {
+ pr_warn("Signed file measurement uses %s, but file uses %s\n",
+ hash_alg->name, vi->hash_alg->name);
+ return -EBADMSG;
+ }
+
+ memcpy(vi->measurement, d->digest, hash_alg->digest_size);
+ vi->have_signed_measurement = true;
+ return 0;
+}
+
+/**
+ * fsverity_parse_pkcs7_signature_extension - verify the signed file measurement
+ *
+ * Verify a signed fsverity_measurement against the certificates in the
+ * fs-verity keyring. The signature is given as a PKCS#7 formatted message, and
+ * the signed data is included in the message (not detached).
+ *
+ * Return: 0 if the signature checks out and the signed measurement is
+ * well-formed and uses the expected hash algorithm; -EBADMSG on signature
+ * verification failure or malformed data; else another -errno code.
+ */
+int fsverity_parse_pkcs7_signature_extension(struct fsverity_info *vi,
+ const void *raw_pkcs7, size_t size)
+{
+ int err;
+
+ if (vi->have_signed_measurement) {
+ pr_warn("Found multiple PKCS#7 signatures\n");
+ return -EBADMSG;
+ }
+
+ if (!vi->hash_alg->cryptographic) {
+ /* Might as well check this... */
+ pr_warn("Found signed %s file measurement, but %s isn't a cryptographic hash algorithm.\n",
+ vi->hash_alg->name, vi->hash_alg->name);
+ return -EBADMSG;
+ }
+
+ err = verify_pkcs7_signature(NULL, 0, raw_pkcs7, size, fsverity_keyring,
+ VERIFYING_UNSPECIFIED_SIGNATURE,
+ extract_measurement, vi);
+ if (err)
+ pr_warn("PKCS#7 signature verification error: %d\n", err);
+
+ return err;
+}
+
+#ifdef CONFIG_SYSCTL
+static int zero;
+static int one = 1;
+static struct ctl_table_header *fsverity_sysctl_header;
+
+static const struct ctl_path fsverity_sysctl_path[] = {
+ { .procname = "fs", },
+ { .procname = "verity", },
+ { }
+};
+
+static struct ctl_table fsverity_sysctl_table[] = {
+ {
+ .procname = "require_signatures",
+ .data = &fsverity_require_signatures,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec_minmax,
+ .extra1 = &zero,
+ .extra2 = &one,
+ },
+ { }
+};
+
+static int __init fsverity_sysctl_init(void)
+{
+ fsverity_sysctl_header = register_sysctl_paths(fsverity_sysctl_path,
+ fsverity_sysctl_table);
+ if (!fsverity_sysctl_header) {
+ pr_warn("sysctl registration failed!");
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static void __exit fsverity_sysctl_exit(void)
+{
+ unregister_sysctl_table(fsverity_sysctl_header);
+}
+#else /* CONFIG_SYSCTL */
+static inline int fsverity_sysctl_init(void)
+{
+ return 0;
+}
+
+static inline void fsverity_sysctl_exit(void)
+{
+}
+#endif /* !CONFIG_SYSCTL */
+
+int __init fsverity_signature_init(void)
+{
+ struct key *ring;
+ int err;
+
+ ring = keyring_alloc(".fs-verity", KUIDT_INIT(0), KGIDT_INIT(0),
+ current_cred(),
+ ((KEY_POS_ALL & ~KEY_POS_SETATTR) |
+ KEY_USR_VIEW | KEY_USR_READ |
+ KEY_USR_WRITE | KEY_USR_SEARCH | KEY_USR_SETATTR),
+ KEY_ALLOC_NOT_IN_QUOTA, NULL, NULL);
+ if (IS_ERR(ring))
+ return PTR_ERR(ring);
+
+ err = fsverity_sysctl_init();
+ if (err)
+ goto error_put_ring;
+
+ fsverity_keyring = ring;
+ return 0;
+
+error_put_ring:
+ key_put(ring);
+ return err;
+}
+
+void __exit fsverity_signature_exit(void)
+{
+ key_put(fsverity_keyring);
+ fsverity_sysctl_exit();
+}
diff --git a/include/uapi/linux/fsverity.h b/include/uapi/linux/fsverity.h
index a96bbf87077de..b030589b8fd93 100644
--- a/include/uapi/linux/fsverity.h
+++ b/include/uapi/linux/fsverity.h
@@ -56,6 +56,7 @@ struct fsverity_descriptor {
/* Extension types */
#define FS_VERITY_EXT_ROOT_HASH 1
#define FS_VERITY_EXT_SALT 2
+#define FS_VERITY_EXT_PKCS7_SIGNATURE 3

/* Header of each extension (variable-length metadata item) */
struct fsverity_extension {
@@ -78,6 +79,15 @@ struct fsverity_extension {

/* FS_VERITY_EXT_SALT payload is just a byte array, any size */

+/*
+ * FS_VERITY_EXT_PKCS7_SIGNATURE payload is a DER-encoded PKCS#7 message
+ * containing the signed file measurement in the following format:
+ */
+struct fsverity_digest_disk {
+ __le16 digest_algorithm;
+ __le16 digest_size;
+ __u8 digest[];
+};

/* Fields stored at the very end of the file */
struct fsverity_footer {
--
2.19.1.568.g152ad8e336-goog