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