kernel_samsung_a34x-permissive/fs/incfs/verity.c
2024-04-28 15:51:13 +02:00

723 lines
18 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2020 Google LLC
*/
/*
* fs-verity integration into incfs
*
* Since incfs has its own merkle tree implementation, most of fs-verity code
* is not needed. The key part that is needed is the signature check, since
* that is based on the private /proc/sys/fs/verity/require_signatures value
* and a private keyring. Thus the first change is to modify verity code to
* export a version of fsverity_verify_signature.
*
* fs-verity integration then consists of the following modifications:
*
* 1. Add the (optional) verity signature to the incfs file format
* 2. Add a pointer to the digest of the fs-verity descriptor struct to the
* data_file struct that incfs attaches to each file inode.
* 3. Add the following ioclts:
* - FS_IOC_ENABLE_VERITY
* - FS_IOC_GETFLAGS
* - FS_IOC_MEASURE_VERITY
* 4. When FS_IOC_ENABLE_VERITY is called on a non-verity file, the
* fs-verity descriptor struct is populated and digested. If it passes the
* signature check or the signature is NULL and
* fs.verity.require_signatures=0, then the S_VERITY flag is set and the
* xattr incfs.verity is set. If the signature is non-NULL, an
* INCFS_MD_VERITY_SIGNATURE is added to the backing file containing the
* signature.
* 5. When a file with an incfs.verity xattr's inode is initialized, the
* inodes S_VERITY flag is set.
* 6. When a file with the S_VERITY flag set on its inode is opened, the
* data_file is checked for its verity digest. If the file doesnt have a
* digest, the files digest is calculated as above, checked, and set, or the
* open is denied if it is not valid.
* 7. FS_IOC_GETFLAGS simply returns the value of the S_VERITY flag
* 8. FS_IOC_MEASURE_VERITY simply returns the cached digest
* 9. The final complication is that if FS_IOC_ENABLE_VERITY is called on a file
* which doesnt have a merkle tree, the merkle tree is calculated before the
* rest of the process is completed.
*/
#include <crypto/hash.h>
#include <crypto/sha.h>
#include <linux/fsverity.h>
#include <linux/mount.h>
#include "verity.h"
#include "data_mgmt.h"
#include "format.h"
#include "integrity.h"
#include "vfs.h"
#define FS_VERITY_MAX_SIGNATURE_SIZE 16128
static int incfs_get_root_hash(struct file *filp, u8 *root_hash)
{
struct data_file *df = get_incfs_data_file(filp);
if (!df)
return -EINVAL;
memcpy(root_hash, df->df_hash_tree->root_hash,
df->df_hash_tree->alg->digest_size);
return 0;
}
static int incfs_end_enable_verity(struct file *filp, u8 *sig, size_t sig_size)
{
struct inode *inode = file_inode(filp);
struct mem_range signature = {
.data = sig,
.len = sig_size,
};
struct data_file *df = get_incfs_data_file(filp);
struct backing_file_context *bfc;
int error;
struct incfs_df_verity_signature *vs;
loff_t offset;
if (!df || !df->df_backing_file_context)
return -EFSCORRUPTED;
vs = kzalloc(sizeof(*vs), GFP_NOFS);
if (!vs)
return -ENOMEM;
bfc = df->df_backing_file_context;
error = mutex_lock_interruptible(&bfc->bc_mutex);
if (error)
goto out;
error = incfs_write_verity_signature_to_backing_file(bfc, signature,
&offset);
mutex_unlock(&bfc->bc_mutex);
if (error)
goto out;
/*
* Set verity xattr so we can set S_VERITY without opening backing file
*/
error = vfs_setxattr(bfc->bc_file->f_path.dentry,
INCFS_XATTR_VERITY_NAME, NULL, 0, XATTR_CREATE);
if (error) {
pr_warn("incfs: error setting verity xattr: %d\n", error);
goto out;
}
*vs = (struct incfs_df_verity_signature) {
.size = signature.len,
.offset = offset,
};
df->df_verity_signature = vs;
vs = NULL;
inode_set_flags(inode, S_VERITY, S_VERITY);
out:
kfree(vs);
return error;
}
static int incfs_compute_file_digest(struct incfs_hash_alg *alg,
struct fsverity_descriptor *desc,
u8 *digest)
{
SHASH_DESC_ON_STACK(d, alg->shash);
d->tfm = alg->shash;
return crypto_shash_digest(d, (u8 *)desc, sizeof(*desc), digest);
}
static enum incfs_hash_tree_algorithm incfs_convert_fsverity_hash_alg(
int hash_alg)
{
switch (hash_alg) {
case FS_VERITY_HASH_ALG_SHA256:
return INCFS_HASH_TREE_SHA256;
default:
return -EINVAL;
}
}
static struct mem_range incfs_get_verity_digest(struct inode *inode)
{
struct inode_info *node = get_incfs_node(inode);
struct data_file *df;
struct mem_range verity_file_digest;
if (!node) {
pr_warn("Invalid inode\n");
return range(NULL, 0);
}
df = node->n_file;
/*
* Pairs with the cmpxchg_release() in incfs_set_verity_digest().
* I.e., another task may publish ->df_verity_file_digest concurrently,
* executing a RELEASE barrier. We need to use smp_load_acquire() here
* to safely ACQUIRE the memory the other task published.
*/
verity_file_digest.data = smp_load_acquire(
&df->df_verity_file_digest.data);
verity_file_digest.len = df->df_verity_file_digest.len;
return verity_file_digest;
}
static void incfs_set_verity_digest(struct inode *inode,
struct mem_range verity_file_digest)
{
struct inode_info *node = get_incfs_node(inode);
struct data_file *df;
if (!node) {
pr_warn("Invalid inode\n");
kfree(verity_file_digest.data);
return;
}
df = node->n_file;
df->df_verity_file_digest.len = verity_file_digest.len;
/*
* Multiple tasks may race to set ->df_verity_file_digest.data, so use
* cmpxchg_release(). This pairs with the smp_load_acquire() in
* incfs_get_verity_digest(). I.e., here we publish
* ->df_verity_file_digest.data, with a RELEASE barrier so that other
* tasks can ACQUIRE it.
*/
if (cmpxchg_release(&df->df_verity_file_digest.data, NULL,
verity_file_digest.data) != NULL)
/* Lost the race, so free the file_digest we allocated. */
kfree(verity_file_digest.data);
}
/*
* Calculate the digest of the fsverity_descriptor. The signature (if present)
* is also checked.
*/
static struct mem_range incfs_calc_verity_digest_from_desc(
const struct inode *inode,
struct fsverity_descriptor *desc,
u8 *signature, size_t sig_size)
{
enum incfs_hash_tree_algorithm incfs_hash_alg;
struct mem_range verity_file_digest;
int err;
struct incfs_hash_alg *hash_alg;
incfs_hash_alg = incfs_convert_fsverity_hash_alg(desc->hash_algorithm);
if (incfs_hash_alg < 0)
return range(ERR_PTR(incfs_hash_alg), 0);
hash_alg = incfs_get_hash_alg(incfs_hash_alg);
if (IS_ERR(hash_alg))
return range((u8 *)hash_alg, 0);
verity_file_digest = range(kzalloc(hash_alg->digest_size, GFP_KERNEL),
hash_alg->digest_size);
if (!verity_file_digest.data)
return range(ERR_PTR(-ENOMEM), 0);
err = incfs_compute_file_digest(hash_alg, desc,
verity_file_digest.data);
if (err) {
pr_err("Error %d computing file digest", err);
goto out;
}
pr_debug("Computed file digest: %s:%*phN\n",
hash_alg->name, (int) verity_file_digest.len,
verity_file_digest.data);
err = __fsverity_verify_signature(inode, signature, sig_size,
verity_file_digest.data,
desc->hash_algorithm);
out:
if (err) {
kfree(verity_file_digest.data);
verity_file_digest = range(ERR_PTR(err), 0);
}
return verity_file_digest;
}
static struct mem_range incfs_calc_verity_digest(
struct inode *inode, struct file *filp,
u8 *signature, size_t signature_size,
int hash_algorithm)
{
struct fsverity_descriptor *desc = kzalloc(sizeof(*desc), GFP_KERNEL);
int err;
struct mem_range verity_file_digest;
if (!desc)
return range(ERR_PTR(-ENOMEM), 0);
*desc = (struct fsverity_descriptor) {
.version = 1,
.hash_algorithm = hash_algorithm,
.log_blocksize = ilog2(INCFS_DATA_FILE_BLOCK_SIZE),
.data_size = cpu_to_le64(inode->i_size),
};
err = incfs_get_root_hash(filp, desc->root_hash);
if (err)
goto out;
verity_file_digest = incfs_calc_verity_digest_from_desc(inode, desc,
signature, signature_size);
out:
kfree(desc);
if (err)
return range(ERR_PTR(err), 0);
return verity_file_digest;
}
static int incfs_build_merkle_tree(struct file *f, struct data_file *df,
struct backing_file_context *bfc,
struct mtree *hash_tree, loff_t hash_offset,
struct incfs_hash_alg *alg, struct mem_range hash)
{
int error = 0;
int limit, lvl, i, result;
struct mem_range buf = {.len = INCFS_DATA_FILE_BLOCK_SIZE};
struct mem_range tmp = {.len = 2 * INCFS_DATA_FILE_BLOCK_SIZE};
buf.data = (u8 *)__get_free_pages(GFP_NOFS, get_order(buf.len));
tmp.data = (u8 *)__get_free_pages(GFP_NOFS, get_order(tmp.len));
if (!buf.data || !tmp.data) {
error = -ENOMEM;
goto out;
}
/*
* lvl - 1 is the level we are reading, lvl the level we are writing
* lvl == -1 means actual blocks
* lvl == hash_tree->depth means root hash
*/
limit = df->df_data_block_count;
for (lvl = 0; lvl <= hash_tree->depth; lvl++) {
for (i = 0; i < limit; ++i) {
loff_t hash_level_offset;
struct mem_range partial_buf = buf;
if (lvl == 0)
result = incfs_read_data_file_block(partial_buf,
f, i, tmp, NULL);
else {
hash_level_offset = hash_offset +
hash_tree->hash_level_suboffset[lvl - 1];
result = incfs_kread(bfc, partial_buf.data,
partial_buf.len,
hash_level_offset + i *
INCFS_DATA_FILE_BLOCK_SIZE);
}
if (result < 0) {
error = result;
goto out;
}
partial_buf.len = result;
error = incfs_calc_digest(alg, partial_buf, hash);
if (error)
goto out;
/*
* last level - only one hash to take and it is stored
* in the incfs signature record
*/
if (lvl == hash_tree->depth)
break;
hash_level_offset = hash_offset +
hash_tree->hash_level_suboffset[lvl];
result = incfs_kwrite(bfc, hash.data, hash.len,
hash_level_offset + hash.len * i);
if (result < 0) {
error = result;
goto out;
}
if (result != hash.len) {
error = -EIO;
goto out;
}
}
limit = DIV_ROUND_UP(limit,
INCFS_DATA_FILE_BLOCK_SIZE / hash.len);
}
out:
free_pages((unsigned long)tmp.data, get_order(tmp.len));
free_pages((unsigned long)buf.data, get_order(buf.len));
return error;
}
/*
* incfs files have a signature record that is separate from the
* verity_signature record. The signature record does not actually contain a
* signature, rather it contains the size/offset of the hash tree, and a binary
* blob which contains the root hash and potentially a signature.
*
* If the file was created with a signature record, then this function simply
* returns.
*
* Otherwise it will create a signature record with a minimal binary blob as
* defined by the structure below, create space for the hash tree and then
* populate it using incfs_build_merkle_tree
*/
static int incfs_add_signature_record(struct file *f)
{
/* See incfs_parse_signature */
struct {
__le32 version;
__le32 size_of_hash_info_section;
struct {
__le32 hash_algorithm;
u8 log2_blocksize;
__le32 salt_size;
u8 salt[0];
__le32 hash_size;
u8 root_hash[32];
} __packed hash_section;
__le32 size_of_signing_info_section;
u8 signing_info_section[0];
} __packed sig = {
.version = cpu_to_le32(INCFS_SIGNATURE_VERSION),
.size_of_hash_info_section =
cpu_to_le32(sizeof(sig.hash_section)),
.hash_section = {
.hash_algorithm = cpu_to_le32(INCFS_HASH_TREE_SHA256),
.log2_blocksize = ilog2(INCFS_DATA_FILE_BLOCK_SIZE),
.hash_size = cpu_to_le32(SHA256_DIGEST_SIZE),
},
};
struct data_file *df = get_incfs_data_file(f);
struct mtree *hash_tree = NULL;
struct backing_file_context *bfc;
int error;
loff_t hash_offset, sig_offset;
struct incfs_hash_alg *alg = incfs_get_hash_alg(INCFS_HASH_TREE_SHA256);
u8 hash_buf[INCFS_MAX_HASH_SIZE];
int hash_size = alg->digest_size;
struct mem_range hash = range(hash_buf, hash_size);
int result;
struct incfs_df_signature *signature = NULL;
if (!df)
return -EINVAL;
if (df->df_header_flags & INCFS_FILE_MAPPED)
return -EINVAL;
/* Already signed? */
if (df->df_signature && df->df_hash_tree)
return 0;
if (df->df_signature || df->df_hash_tree)
return -EFSCORRUPTED;
/* Add signature metadata record to file */
hash_tree = incfs_alloc_mtree(range((u8 *)&sig, sizeof(sig)),
df->df_data_block_count);
if (IS_ERR(hash_tree))
return PTR_ERR(hash_tree);
bfc = df->df_backing_file_context;
if (!bfc) {
error = -EFSCORRUPTED;
goto out;
}
error = mutex_lock_interruptible(&bfc->bc_mutex);
if (error)
goto out;
error = incfs_write_signature_to_backing_file(bfc,
range((u8 *)&sig, sizeof(sig)),
hash_tree->hash_tree_area_size,
&hash_offset, &sig_offset);
mutex_unlock(&bfc->bc_mutex);
if (error)
goto out;
/* Populate merkle tree */
error = incfs_build_merkle_tree(f, df, bfc, hash_tree, hash_offset, alg,
hash);
if (error)
goto out;
/* Update signature metadata record */
memcpy(sig.hash_section.root_hash, hash.data, alg->digest_size);
result = incfs_kwrite(bfc, &sig, sizeof(sig), sig_offset);
if (result < 0) {
error = result;
goto out;
}
if (result != sizeof(sig)) {
error = -EIO;
goto out;
}
/* Update in-memory records */
memcpy(hash_tree->root_hash, hash.data, alg->digest_size);
signature = kzalloc(sizeof(*signature), GFP_NOFS);
if (!signature) {
error = -ENOMEM;
goto out;
}
*signature = (struct incfs_df_signature) {
.hash_offset = hash_offset,
.hash_size = hash_tree->hash_tree_area_size,
.sig_offset = sig_offset,
.sig_size = sizeof(sig),
};
df->df_signature = signature;
signature = NULL;
/*
* Use memory barrier to prevent readpage seeing the hash tree until
* it's fully there
*/
smp_store_release(&df->df_hash_tree, hash_tree);
hash_tree = NULL;
out:
kfree(signature);
kfree(hash_tree);
return error;
}
static int incfs_enable_verity(struct file *filp,
const struct fsverity_enable_arg *arg)
{
struct inode *inode = file_inode(filp);
struct data_file *df = get_incfs_data_file(filp);
u8 *signature = NULL;
struct mem_range verity_file_digest = range(NULL, 0);
int err;
if (!df)
return -EFSCORRUPTED;
err = mutex_lock_interruptible(&df->df_enable_verity);
if (err)
return err;
if (IS_VERITY(inode)) {
err = -EEXIST;
goto out;
}
err = incfs_add_signature_record(filp);
if (err)
goto out;
/* Get the signature if the user provided one */
if (arg->sig_size) {
signature = memdup_user(u64_to_user_ptr(arg->sig_ptr),
arg->sig_size);
if (IS_ERR(signature)) {
err = PTR_ERR(signature);
signature = NULL;
goto out;
}
}
verity_file_digest = incfs_calc_verity_digest(inode, filp, signature,
arg->sig_size, arg->hash_algorithm);
if (IS_ERR(verity_file_digest.data)) {
err = PTR_ERR(verity_file_digest.data);
verity_file_digest.data = NULL;
goto out;
}
err = incfs_end_enable_verity(filp, signature, arg->sig_size);
if (err)
goto out;
/* Successfully enabled verity */
incfs_set_verity_digest(inode, verity_file_digest);
verity_file_digest.data = NULL;
out:
mutex_unlock(&df->df_enable_verity);
kfree(signature);
kfree(verity_file_digest.data);
if (err)
pr_err("%s failed with err %d\n", __func__, err);
return err;
}
int incfs_ioctl_enable_verity(struct file *filp, const void __user *uarg)
{
struct inode *inode = file_inode(filp);
struct fsverity_enable_arg arg;
if (copy_from_user(&arg, uarg, sizeof(arg)))
return -EFAULT;
if (arg.version != 1)
return -EINVAL;
if (arg.__reserved1 ||
memchr_inv(arg.__reserved2, 0, sizeof(arg.__reserved2)))
return -EINVAL;
if (arg.hash_algorithm != FS_VERITY_HASH_ALG_SHA256)
return -EINVAL;
if (arg.block_size != PAGE_SIZE)
return -EINVAL;
if (arg.salt_size)
return -EINVAL;
if (arg.sig_size > FS_VERITY_MAX_SIGNATURE_SIZE)
return -EMSGSIZE;
if (S_ISDIR(inode->i_mode))
return -EISDIR;
if (!S_ISREG(inode->i_mode))
return -EINVAL;
return incfs_enable_verity(filp, &arg);
}
static u8 *incfs_get_verity_signature(struct file *filp, size_t *sig_size)
{
struct data_file *df = get_incfs_data_file(filp);
struct incfs_df_verity_signature *vs;
u8 *signature;
int res;
if (!df || !df->df_backing_file_context)
return ERR_PTR(-EFSCORRUPTED);
vs = df->df_verity_signature;
if (!vs) {
*sig_size = 0;
return NULL;
}
signature = kzalloc(vs->size, GFP_KERNEL);
if (!signature)
return ERR_PTR(-ENOMEM);
res = incfs_kread(df->df_backing_file_context,
signature, vs->size, vs->offset);
if (res < 0)
goto err_out;
if (res != vs->size) {
res = -EINVAL;
goto err_out;
}
*sig_size = vs->size;
return signature;
err_out:
kfree(signature);
return ERR_PTR(res);
}
/* Ensure data_file->df_verity_file_digest is populated */
static int ensure_verity_info(struct inode *inode, struct file *filp)
{
struct mem_range verity_file_digest;
u8 *signature = NULL;
size_t sig_size;
int err = 0;
/* See if this file's verity file digest is already cached */
verity_file_digest = incfs_get_verity_digest(inode);
if (verity_file_digest.data)
return 0;
signature = incfs_get_verity_signature(filp, &sig_size);
if (IS_ERR(signature))
return PTR_ERR(signature);
verity_file_digest = incfs_calc_verity_digest(inode, filp, signature,
sig_size,
FS_VERITY_HASH_ALG_SHA256);
if (IS_ERR(verity_file_digest.data)) {
err = PTR_ERR(verity_file_digest.data);
goto out;
}
incfs_set_verity_digest(inode, verity_file_digest);
out:
kfree(signature);
return err;
}
/**
* incfs_fsverity_file_open() - prepare to open a file that may be
* verity-enabled
* @inode: the inode being opened
* @filp: the struct file being set up
*
* When opening a verity file, set up data_file->df_verity_file_digest if not
* already done. Note that incfs does not allow opening for writing, so there is
* no need for that check.
*
* Return: 0 on success, -errno on failure
*/
int incfs_fsverity_file_open(struct inode *inode, struct file *filp)
{
if (IS_VERITY(inode))
return ensure_verity_info(inode, filp);
return 0;
}
int incfs_ioctl_measure_verity(struct file *filp, void __user *_uarg)
{
struct inode *inode = file_inode(filp);
struct mem_range verity_file_digest = incfs_get_verity_digest(inode);
struct fsverity_digest __user *uarg = _uarg;
struct fsverity_digest arg;
if (!verity_file_digest.data || !verity_file_digest.len)
return -ENODATA; /* not a verity file */
/*
* The user specifies the digest_size their buffer has space for; we can
* return the digest if it fits in the available space. We write back
* the actual size, which may be shorter than the user-specified size.
*/
if (get_user(arg.digest_size, &uarg->digest_size))
return -EFAULT;
if (arg.digest_size < verity_file_digest.len)
return -EOVERFLOW;
memset(&arg, 0, sizeof(arg));
arg.digest_algorithm = FS_VERITY_HASH_ALG_SHA256;
arg.digest_size = verity_file_digest.len;
if (copy_to_user(uarg, &arg, sizeof(arg)))
return -EFAULT;
if (copy_to_user(uarg->digest, verity_file_digest.data,
verity_file_digest.len))
return -EFAULT;
return 0;
}