/* * This code is based on IMA's code * * Copyright (C) 2016 Samsung Electronics, Inc. * * Egor Uleyskiy, * Viacheslav Vovchenko * Yevgen Kopylov * * This software is licensed under the terms of the GNU General Public * License version 2, as published by the Free Software Foundation, and * may be copied, distributed, and modified under those terms. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include #include #include #include #include #include #include "five.h" #include "five_audit.h" #include "five_hooks.h" #include "five_tee_api.h" #include "five_porting.h" #include "five_cache.h" #include "five_dmverity.h" #define FIVE_RSA_SIGNATURE_MAX_LENGTH (2048/8) /* Identify extend structure of integrity label */ #define FIVE_ID_INTEGRITY_LABEL_EX 0xFFFF #define FIVE_LABEL_VERSION1 1 /* Maximum length of data integrity label. * This limit is applied because: * 1. TEEgris doesn't support signing data longer than 480 bytes; * 2. The label's length is limited to 3965 byte according to the data * transmission protocol between five_tee_driver and TA. */ #define FIVE_LABEL_MAX_LEN 256 /** * Extend structure of integrity label. * If field "len" equals 0xffff then it is extend integrity label, * otherwise simple integrity label. */ struct integrity_label_ex { uint16_t len; uint8_t version; uint8_t reserved[2]; uint8_t hash_algo; uint8_t hash[64]; struct integrity_label label; } __packed; #ifndef CONFIG_SAMSUNG_PRODUCT_SHIP static const bool panic_on_error = true; #else static const bool panic_on_error; #endif static DECLARE_RWSEM(sign_fcntl_lock); /* * five_collect_measurement - collect file measurement * * Must be called with iint->mutex held. * * Return 0 on success, error code otherwise. */ static int five_collect_measurement(struct file *file, u8 hash_algo, u8 *hash, size_t hash_len) { int result = 0; BUG_ON(!file || !hash); result = five_calc_file_hash(file, hash_algo, hash, &hash_len); if (result) { five_audit_err(current, file, "collect_measurement", 0, 0, "calculate file hash failed", result); } return result; } static int get_integrity_label(struct five_cert *cert, void **label_data, size_t *label_len) { int rc = -ENODATA; struct five_cert_header *header = (struct five_cert_header *)cert->body.header->value; if (header && header->signature_type == FIVE_XATTR_HMAC) { *label_data = cert->body.label->value; *label_len = cert->body.label->length; rc = 0; } return rc; } static int get_signature(struct five_cert *cert, void **sig, size_t *sig_len) { int rc = -ENODATA; struct five_cert_header *header = (struct five_cert_header *)cert->body.header->value; if (header && header->signature_type == FIVE_XATTR_HMAC) { if (cert->signature->length == 0) return rc; *sig = cert->signature->value; *sig_len = cert->signature->length; rc = 0; } return rc; } static int update_label(struct integrity_iint_cache *iint, const void *label_data, size_t label_len) { struct integrity_label *l; if (!label_data) return 0; l = kmalloc(sizeof(struct integrity_label) + label_len, GFP_NOFS); if (l) { l->len = label_len; memcpy(l->data, label_data, label_len); kfree(iint->five_label); iint->five_label = l; } else { return -ENOMEM; } return 0; } static int five_fix_xattr(struct task_struct *task, struct dentry *dentry, struct file *file, void **raw_cert, size_t *raw_cert_len, struct integrity_iint_cache *iint, struct integrity_label_ex *label) { int rc = 0; u8 hash[FIVE_MAX_DIGEST_SIZE], *hash_file, *sig = NULL; size_t hash_len = sizeof(hash), hash_file_len, sig_len; void *file_label = label->label.data; u16 file_label_len = label->label.len; struct five_cert_body body_cert = {0}; struct five_cert_header *header; BUG_ON(!task || !dentry || !file || !raw_cert || !(*raw_cert) || !iint); BUG_ON(!raw_cert_len); rc = five_cert_body_fillout(&body_cert, *raw_cert, *raw_cert_len); if (unlikely(rc)) return -EINVAL; header = (struct five_cert_header *)body_cert.header->value; hash_file = body_cert.hash->value; hash_file_len = body_cert.hash->length; if (unlikely(!header || !hash_file)) return -EINVAL; if (label->version == FIVE_LABEL_VERSION1) { rc = five_collect_measurement(file, header->hash_algo, hash_file, hash_file_len); if (unlikely(rc)) return rc; } else { memcpy(hash_file, label->hash, hash_file_len); } rc = five_cert_calc_hash(&body_cert, hash, &hash_len); if (unlikely(rc)) return rc; sig_len = (size_t)body_cert.hash->length + file_label_len; sig = kzalloc(sig_len, GFP_NOFS); if (!sig) return -ENOMEM; rc = sign_hash(header->hash_algo, hash, hash_len, file_label, file_label_len, sig, &sig_len); if (!rc) { rc = five_cert_append_signature(raw_cert, raw_cert_len, sig, sig_len); if (!rc) { int count = 1; do { rc = __vfs_setxattr_noperm(d_real_comp(dentry), XATTR_NAME_FIVE, *raw_cert, *raw_cert_len, 0); count--; } while (count >= 0 && rc != 0); if (!rc) { rc = update_label(iint, file_label, file_label_len); } } } else if (panic_on_error) { panic("FIVE failed to sign %s (ret code = %d)", dentry->d_name.name, rc); } else { five_audit_sign_err(current, file, "fix_xattr", 0, 0, "can't sign the file", rc); } kfree(sig); return rc; } int five_read_xattr(struct dentry *dentry, char **xattr_value) { ssize_t ret; ret = vfs_getxattr_alloc(dentry, XATTR_NAME_FIVE, xattr_value, 0, GFP_NOFS); if (ret < 0) ret = 0; return ret; } static bool bad_fs(struct inode *inode) { if (inode->i_sb->s_magic == EXT4_SUPER_MAGIC || inode->i_sb->s_magic == F2FS_SUPER_MAGIC || inode->i_sb->s_magic == OVERLAYFS_SUPER_MAGIC || inode->i_sb->s_magic == EROFS_SUPER_MAGIC_V1) return false; return true; } static bool readonly_sb(struct inode *inode) { if (inode->i_sb->s_flags & MS_RDONLY) return true; return false; } /* * five_is_fsverity_protected - checks if file is protected by FSVERITY * * Return true/false */ static bool five_is_fsverity_protected(const struct inode *inode) { return IS_VERITY(inode); } /* * five_appraise_measurement - appraise file measurement * * Return 0 on success, error code otherwise */ int five_appraise_measurement(struct task_struct *task, int func, struct integrity_iint_cache *iint, struct file *file, struct five_cert *cert) { enum task_integrity_reset_cause cause = CAUSE_UNKNOWN; struct dentry *dentry = NULL; struct inode *inode = NULL; enum five_file_integrity status = FIVE_FILE_UNKNOWN; enum task_integrity_value prev_integrity; int rc = 0; u8 *file_hash; u8 stored_file_hash[FIVE_MAX_DIGEST_SIZE] = {0}; size_t file_hash_len = 0; struct five_cert_header *header = NULL; BUG_ON(!task || !iint || !file); prev_integrity = task_integrity_read(TASK_INTEGRITY(task)); dentry = file->f_path.dentry; inode = d_backing_inode(dentry); if (bad_fs(inode)) { status = FIVE_FILE_FAIL; cause = CAUSE_BAD_FS; rc = -ENOTSUPP; goto out; } if (!cert) { cause = CAUSE_NO_CERT; if (five_is_fsverity_protected(inode)) status = FIVE_FILE_FSVERITY; else if (five_is_dmverity_protected(file)) status = FIVE_FILE_DMVERITY; goto out; } header = (struct five_cert_header *)cert->body.header->value; file_hash = cert->body.hash->value; file_hash_len = cert->body.hash->length; if (file_hash_len > sizeof(stored_file_hash)) { cause = CAUSE_INVALID_HASH_LENGTH; rc = -EINVAL; goto out; } memcpy(stored_file_hash, file_hash, file_hash_len); if (unlikely(!header || !file_hash)) { cause = CAUSE_INVALID_HEADER; rc = -EINVAL; goto out; } rc = five_collect_measurement(file, header->hash_algo, file_hash, file_hash_len); if (rc) { cause = CAUSE_CALC_HASH_FAILED; goto out; } switch (header->signature_type) { case FIVE_XATTR_HMAC: { u8 *sig = NULL; u8 algo = header->hash_algo; void *file_label_data; size_t file_label_len, sig_len = 0; u8 cert_hash[FIVE_MAX_DIGEST_SIZE] = {0}; size_t cert_hash_len = sizeof(cert_hash); status = FIVE_FILE_FAIL; rc = get_integrity_label(cert, &file_label_data, &file_label_len); if (unlikely(rc)) { cause = CAUSE_INVALID_LABEL_DATA; break; } if (unlikely(file_label_len > PAGE_SIZE)) { cause = CAUSE_INVALID_LABEL_DATA; break; } rc = get_signature(cert, (void **)&sig, &sig_len); if (unlikely(rc)) { cause = CAUSE_INVALID_SIGNATURE_DATA; break; } rc = five_cert_calc_hash(&cert->body, cert_hash, &cert_hash_len); if (unlikely(rc)) { cause = CAUSE_INVALID_CALC_CERT_HASH; break; } rc = verify_hash(algo, cert_hash, cert_hash_len, file_label_data, file_label_len, sig, sig_len); if (unlikely(rc)) { cause = CAUSE_INVALID_HASH; if (cert) { five_audit_hexinfo(file, "stored hash", stored_file_hash, file_hash_len); five_audit_hexinfo(file, "calculated hash", file_hash, file_hash_len); five_audit_hexinfo(file, "HMAC signature", sig, sig_len); } break; } rc = update_label(iint, file_label_data, file_label_len); if (unlikely(rc)) { cause = CAUSE_INVALID_UPDATE_LABEL; break; } status = FIVE_FILE_HMAC; break; } case FIVE_XATTR_DIGSIG: { u8 cert_hash[FIVE_MAX_DIGEST_SIZE] = {0}; size_t cert_hash_len = sizeof(cert_hash); status = FIVE_FILE_FAIL; rc = five_cert_calc_hash(&cert->body, cert_hash, &cert_hash_len); if (unlikely(rc)) { cause = CAUSE_INVALID_CALC_CERT_HASH; break; } rc = five_digsig_verify(cert, cert_hash, cert_hash_len); if (rc) { cause = CAUSE_INVALID_SIGNATURE; if (cert) { five_audit_hexinfo(file, "stored hash", stored_file_hash, file_hash_len); five_audit_hexinfo(file, "calculated hash", file_hash, file_hash_len); five_audit_hexinfo(file, "RSA signature", cert->signature->value, cert->signature->length); } break; } status = FIVE_FILE_RSA; break; } default: status = FIVE_FILE_FAIL; cause = CAUSE_UKNOWN_FIVE_DATA; break; } out: if (status == FIVE_FILE_FAIL || status == FIVE_FILE_UNKNOWN) { task_integrity_set_reset_reason(TASK_INTEGRITY(task), cause, file); five_audit_verbose(task, file, five_get_string_fn(func), prev_integrity, prev_integrity, tint_reset_cause_to_string(cause), rc); } five_set_cache_status(iint, status); return rc; } /* * five_update_xattr - update 'security.five' hash value */ static int five_update_xattr(struct task_struct *task, struct integrity_iint_cache *iint, struct file *file, struct integrity_label_ex *label) { struct dentry *dentry; int rc = 0; uint8_t *hash; size_t hash_len; uint8_t *raw_cert; size_t raw_cert_len; struct five_cert_header header = { .version = FIVE_CERT_VERSION1, .privilege = FIVE_PRIV_DEFAULT, .hash_algo = five_hash_algo, .signature_type = FIVE_XATTR_HMAC }; BUG_ON(!task || !iint || !file || !label); if (label->version == FIVE_LABEL_VERSION1) { hash_len = (size_t)hash_digest_size[five_hash_algo]; } else { header.hash_algo = label->hash_algo; if (label->hash_algo >= HASH_ALGO__LAST) return -EINVAL; hash_len = (size_t)hash_digest_size[label->hash_algo]; if (hash_len > sizeof(label->hash)) return -EINVAL; } hash = kzalloc(hash_len, GFP_KERNEL); if (!hash) return -ENOMEM; dentry = file->f_path.dentry; /* do not collect and update hash for digital signatures */ if (five_get_cache_status(iint) == FIVE_FILE_RSA) { char dummy[512]; struct inode *inode = file_inode(file); rc = __vfs_getxattr(d_real_comp(dentry), inode, XATTR_NAME_FIVE, dummy, sizeof(dummy), XATTR_NOSECURITY); // Check if xattr is exist if (rc > 0 || rc != -ENODATA) { kfree(hash); return -EPERM; } else { // xattr does not exist. five_set_cache_status(iint, FIVE_FILE_UNKNOWN); pr_err("FIVE: ERROR: Cache is unsynchronized"); } } rc = five_cert_body_alloc(&header, hash, hash_len, label->label.data, label->label.len, &raw_cert, &raw_cert_len); if (rc) goto exit; if (task_integrity_allow_sign(TASK_INTEGRITY(task))) { rc = five_fix_xattr(task, dentry, file, (void **)&raw_cert, &raw_cert_len, iint, label); if (rc) pr_err("FIVE: Can't sign hash: rc=%d\n", rc); } else { rc = -EPERM; } five_hook_file_signed(task, file, raw_cert, raw_cert_len, rc); five_cert_free(raw_cert); exit: kfree(hash); return rc; } static void five_reset_appraise_flags(struct dentry *dentry) { struct inode *inode = d_backing_inode(dentry); struct integrity_iint_cache *iint; if (!S_ISREG(inode->i_mode)) return; iint = integrity_iint_find(inode); if (iint) five_set_cache_status(iint, FIVE_FILE_UNKNOWN); } /** * five_inode_post_setattr - reflect file metadata changes * @dentry: pointer to the affected dentry * * Changes to a dentry's metadata might result in needing to appraise. * * This function is called from notify_change(), which expects the caller * to lock the inode's i_mutex. */ void five_inode_post_setattr(struct task_struct *task, struct dentry *dentry) { five_reset_appraise_flags(dentry); } /* * five_protect_xattr - protect 'security.five' * * Ensure that not just anyone can modify or remove 'security.five'. */ static int five_protect_xattr(struct dentry *dentry, const char *xattr_name, const void *xattr_value, size_t xattr_value_len) { if (strcmp(xattr_name, XATTR_NAME_FIVE) == 0) { if (!capable(CAP_SYS_ADMIN)) return -EPERM; return 1; } return 0; } int five_inode_setxattr(struct dentry *dentry, const char *xattr_name, const void *xattr_value, size_t xattr_value_len) { int result = five_protect_xattr(dentry, xattr_name, xattr_value, xattr_value_len); if (result == 1 && xattr_value_len == 0) { five_reset_appraise_flags(dentry); return 0; } if (result == 1) { bool digsig; struct five_cert_header *header; struct five_cert cert = { {0} }; result = five_cert_fillout(&cert, xattr_value, xattr_value_len); if (result) return result; header = (struct five_cert_header *)cert.body.header->value; if (!xattr_value_len || !header || (header->signature_type >= FIVE_XATTR_END)) return -EINVAL; digsig = (header->signature_type == FIVE_XATTR_DIGSIG); if (!digsig) return -EPERM; five_reset_appraise_flags(dentry); result = 0; } return result; } int five_inode_removexattr(struct dentry *dentry, const char *xattr_name) { int result; result = five_protect_xattr(dentry, xattr_name, NULL, 0); if (result == 1) { five_reset_appraise_flags(dentry); result = 0; } return result; } int five_reboot_notifier(struct notifier_block *nb, unsigned long action, void *unused) { down_write(&sign_fcntl_lock); /* Need to wait for five_fcntl_sign finish */ up_write(&sign_fcntl_lock); return NOTIFY_DONE; } static int copy_label(const struct integrity_label __user *ulabel, struct integrity_label_ex **out_label) { u16 len; size_t label_len; int rc = 0; struct integrity_label_ex header = {0}; struct integrity_label_ex *label = NULL; if (unlikely(!ulabel || !out_label)) { rc = -EINVAL; goto error; } if (unlikely(copy_from_user(&len, ulabel, sizeof(len)))) { rc = -EFAULT; goto error; } if (len == FIVE_ID_INTEGRITY_LABEL_EX) { if (unlikely(copy_from_user(&header, ulabel, sizeof(header)))) { rc = -EFAULT; goto error; } if (len != header.len || header.label.len > FIVE_LABEL_MAX_LEN || header.version <= FIVE_LABEL_VERSION1) { rc = -EINVAL; goto error; } label_len = sizeof(header) + header.label.len; label = kzalloc(label_len, GFP_NOFS); if (unlikely(!label)) { rc = -ENOMEM; goto error; } memcpy(label, &header, sizeof(header)); if (unlikely(copy_from_user(&label->label.data[0], (const u8 __user *)ulabel + sizeof(header), label_len - sizeof(header)))) { rc = -EFAULT; goto error; } } else { if (len > FIVE_LABEL_MAX_LEN) { rc = -EINVAL; goto error; } label_len = sizeof(header) + len; label = kzalloc(label_len, GFP_NOFS); if (unlikely(!label)) { rc = -ENOMEM; goto error; } if (unlikely(copy_from_user(&label->label, ulabel, sizeof(len) + len))) { rc = -EFAULT; goto error; } if (len != label->label.len) { rc = -EINVAL; goto error; } label->version = FIVE_LABEL_VERSION1; } *out_label = label; error: if (rc) kfree(label); return rc; } /* Called from do_fcntl */ int five_fcntl_sign(struct file *file, struct integrity_label __user *label) { struct integrity_iint_cache *iint; struct inode *inode = file_inode(file); struct integrity_label_ex *l = NULL; int rc = 0; if (!S_ISREG(inode->i_mode)) return -EINVAL; if (readonly_sb(inode)) { pr_err("FIVE: Can't sign a file on RO FS\n"); return -EROFS; } if (task_integrity_allow_sign(TASK_INTEGRITY(current))) { rc = copy_label(label, &l); if (rc) { pr_err("FIVE: Can't copy integrity label\n"); return rc; } } else { enum task_integrity_value tint = task_integrity_read(TASK_INTEGRITY(current)); five_audit_err(current, file, "fcntl_sign", tint, tint, "sign:no-perm", -EPERM); return -EPERM; } iint = integrity_inode_get(inode); if (!iint) { kfree(l); return -ENOMEM; } if (file->f_op && file->f_op->flush) { if (file->f_op->flush(file, current->files)) { kfree(l); return -EOPNOTSUPP; } } down_read(&sign_fcntl_lock); inode_lock(inode); rc = five_update_xattr(current, iint, file, l); iint->five_signing = false; inode_unlock(inode); up_read(&sign_fcntl_lock); kfree(l); return rc; } static int check_input_inode(struct inode *inode) { if (!S_ISREG(inode->i_mode)) return -EINVAL; if (readonly_sb(inode)) { pr_err("FIVE: Can't sign a file on RO FS\n"); return -EROFS; } return 0; } int five_fcntl_edit(struct file *file) { int rc; struct dentry *dentry; uint8_t *raw_cert = NULL; size_t raw_cert_len = 0; struct integrity_iint_cache *iint; struct inode *inode = file_inode(file); rc = check_input_inode(inode); if (rc) return rc; if (!task_integrity_allow_sign(TASK_INTEGRITY(current))) return -EPERM; inode_lock(inode); dentry = file->f_path.dentry; rc = __vfs_setxattr_noperm(d_real_comp(dentry), XATTR_NAME_FIVE, raw_cert, raw_cert_len, 0); iint = integrity_inode_get(inode); if (iint) iint->five_signing = true; inode_unlock(inode); return rc; } int five_fcntl_close(struct file *file) { int rc; ssize_t xattr_len; struct dentry *dentry; struct integrity_iint_cache *iint; struct inode *inode = file_inode(file); rc = check_input_inode(inode); if (rc) return rc; inode_lock(inode); iint = integrity_inode_get(inode); if (!iint) { inode_unlock(inode); return -ENOMEM; } if (iint->five_signing) { dentry = file->f_path.dentry; xattr_len = __vfs_getxattr(d_real_comp(dentry), inode, XATTR_NAME_FIVE, NULL, 0, XATTR_NOSECURITY); if (xattr_len == 0) rc = __vfs_removexattr(d_real_comp(dentry), XATTR_NAME_FIVE); iint->five_signing = false; } inode_unlock(inode); return rc; }