* Copyright (c) 2023-2024 Huawei Device Co., Ltd.
*/
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/types.h>
#include <linux/compat.h>
#include <linux/version.h>
#include "avc.h"
#include "objsec.h"
#include "code_sign_ioctl.h"
#include "code_sign_log.h"
#define MAX_SIGNING_LENGTH 2048
DEFINE_SPINLOCK(cert_chain_tree_lock);
struct rb_root cert_chain_tree = RB_ROOT;
struct rb_root dev_cert_chain_tree = RB_ROOT;
struct cert_source *matched_cert_search(struct rb_root *root, const char *subject, const char *issuer)
{
struct rb_node **cur_node = &(root->rb_node);
while (*cur_node) {
struct cert_source *cur_cert = container_of(*cur_node, struct cert_source, node);
int result = strcmp(subject, cur_cert->subject);
if (result < 0) {
cur_node = &((*cur_node)->rb_left);
} else if (result > 0) {
cur_node = &((*cur_node)->rb_right);
} else {
result = strcmp(issuer, cur_cert->issuer);
if (result < 0) {
cur_node = &((*cur_node)->rb_left);
} else if (result > 0) {
cur_node = &((*cur_node)->rb_right);
} else {
code_sign_log_info("cert found");
return cur_cert;
}
}
}
code_sign_log_error("cert not found");
return NULL;
}
struct cert_source *cert_chain_search(struct rb_root *root, const char *subject, const char *issuer, bool has_locked)
{
if (has_locked)
return matched_cert_search(root, subject, issuer);
else {
spin_lock(&cert_chain_tree_lock);
struct cert_source *matched_cert = matched_cert_search(root, subject, issuer);
spin_unlock(&cert_chain_tree_lock);
return matched_cert;
}
}
struct cert_source *find_match(const char *subject, const char *issuer, bool is_dev)
{
if (is_dev)
return cert_chain_search(&dev_cert_chain_tree, subject, issuer, false);
else
return cert_chain_search(&cert_chain_tree, subject, issuer, false);
}
int code_sign_check_caller(char *caller)
{
u32 sid = current_sid(), context_len;
char *context = NULL;
int rc;
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)
rc = security_sid_to_context(&selinux_state, sid, &context, &context_len);
#else
rc = security_sid_to_context(sid, &context, &context_len);
#endif
if (rc)
return -EINVAL;
code_sign_log_debug("sid=%d, context=%s", sid, context);
if (!strncmp(caller, context, strlen(caller)))
return 0;
return -EPERM;
}
int cert_chain_insert(struct rb_root *root, struct cert_source *cert)
{
int ret = code_sign_check_caller(KEY_ENABLE_CTX);
if (ret == -EINVAL) {
code_sign_log_error("load SELinux context failed");
return -EINVAL;
} else if (ret == -EPERM) {
if (!(cert->path_type == RELEASE_DEVELOPER_CODE
|| cert->path_type == DEBUG_DEVELOPER_CODE)) {
code_sign_log_error("no permission to insert code %d", cert->path_type);
return -EPERM;
}
}
spin_lock(&cert_chain_tree_lock);
struct rb_node **new = &(root->rb_node), *parent = NULL;
while (*new) {
struct cert_source *this = container_of(*new, struct cert_source, node);
int result = strcmp(cert->subject, this->subject);
parent = *new;
if (result < 0) {
new = &((*new)->rb_left);
} else if (result > 0) {
new = &((*new)->rb_right);
} else {
result = strcmp(cert->issuer, this->issuer);
if (result < 0) {
new = &((*new)->rb_left);
} else if (result > 0) {
new = &((*new)->rb_right);
} else {
this->cnt++;
code_sign_log_info("cert already exist in trust sources");
goto out;
}
}
}
cert->cnt++;
rb_link_node(&cert->node, parent, new);
rb_insert_color(&cert->node, root);
code_sign_log_info("add trusted cert: subject = '%s', issuer = '%s', max_path_depth = %d",
cert->subject, cert->issuer, cert->max_path_depth);
out:
spin_unlock(&cert_chain_tree_lock);
return 0;
}
int cert_chain_remove(struct rb_root *root, struct cert_source *cert)
{
spin_lock(&cert_chain_tree_lock);
struct cert_source *matched_cert = cert_chain_search(root, cert->subject, cert->issuer, true);
int ret = 0;
if (!matched_cert) {
ret = -EINVAL;
goto out;
}
if (matched_cert->path_type == RELEASE_DEVELOPER_CODE
|| matched_cert->path_type == DEBUG_DEVELOPER_CODE) {
--matched_cert->cnt;
if (matched_cert->cnt > 0)
goto out;
rb_erase(&matched_cert->node, root);
code_sign_log_info("remove trusted cert: subject = '%s', issuer = '%s', max_path_depth = %d",
cert->subject, cert->issuer, cert->max_path_depth);
goto out;
}
code_sign_log_error("can not remove cert type %x", cert->path_type);
ret = -EKEYREJECTED;
out:
spin_unlock(&cert_chain_tree_lock);
return ret;
}
int code_sign_open(struct inode *inode, struct file *filp)
{
return 0;
}
int code_sign_release(struct inode *inode, struct file *filp)
{
return 0;
}
int code_sign_avc_has_perm(u16 tclass, u32 requested)
{
struct av_decision avd;
u32 sid = current_sid();
int rc, rc2;
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)
rc = avc_has_perm_noaudit(&selinux_state, sid, sid, tclass, requested,
AVC_STRICT, &avd);
rc2 = avc_audit(&selinux_state, sid, sid, tclass, requested, &avd, rc,
NULL, AVC_STRICT);
#else
rc = avc_has_perm_noaudit(sid, sid, tclass, requested,
AVC_STRICT, &avd);
rc2 = avc_audit(sid, sid, tclass, requested, &avd, rc,
NULL);
#endif
if (rc2)
return rc2;
return rc;
}
int parse_cert_source(unsigned long args, struct cert_source **_source)
{
int ret = 0;
struct cert_source *source = kzalloc(sizeof(struct cert_source), GFP_KERNEL);
if (!source)
return -ENOMEM;
struct cert_chain_info info;
if (copy_from_user(&info, args, sizeof(struct cert_chain_info))) {
code_sign_log_error("cmd copy_from_user failed");
ret = -ENOMEM;
goto copy_source_failed;
}
if (info.path_len > CERT_CHAIN_PATH_LEN_MAX || info.issuer_length == 0 || info.signing_length == 0
|| info.issuer_length > MAX_SIGNING_LENGTH || info.signing_length > MAX_SIGNING_LENGTH) {
code_sign_log_error("invalid path len or subject or issuer");
ret = -EINVAL;
goto copy_source_failed;
}
source->subject = kzalloc(info.signing_length + 1, GFP_KERNEL);
if (!source->subject) {
ret = -ENOMEM;
goto copy_source_failed;
}
if (copy_from_user(source->subject, u64_to_user_ptr(info.signing_ptr), info.signing_length)) {
code_sign_log_error("copy_from_user get signing failed");
ret = -EFAULT;
goto copy_subject_failed;
}
source->issuer = kzalloc(info.issuer_length + 1, GFP_KERNEL);
if (!source->issuer) {
ret = -ENOMEM;
goto copy_subject_failed;
}
ret = copy_from_user(source->issuer, u64_to_user_ptr(info.issuer_ptr), info.issuer_length);
if (ret) {
code_sign_log_error("copy_from_user get issuer failed");
ret = -EFAULT;
goto copy_issuer_failed;
}
source->max_path_depth = info.path_len;
source->path_type = info.cert_type;
*_source = source;
return ret;
copy_issuer_failed:
kfree(source->issuer);
copy_subject_failed:
kfree(source->subject);
copy_source_failed:
kfree(source);
return ret;
}
int code_sign_check_code(int code)
{
if (code > RELEASE_CODE_START && code < RELEASE_CODE_END)
return 0;
if (code > DEBUG_CODE_START && code < DEBUG_CODE_END)
return 1;
code_sign_log_error("cert type %x is invalid", code);
return -EINVAL;
}
long code_sign_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
int ret = 0;
struct cert_source *source;
switch (cmd) {
case ADD_CERT_CHAIN:
if (code_sign_avc_has_perm(SECCLASS_CODE_SIGN, CODE_SIGN__ADD_CERT_CHAIN)) {
code_sign_log_error("selinux check failed, no permission to add cert chain");
return -EPERM;
}
ret = parse_cert_source(args, &source);
if (ret)
return ret;
ret = code_sign_check_code(source->path_type);
if (ret < 0)
return ret;
if (ret == 1) {
code_sign_log_debug("add developer cert");
ret = cert_chain_insert(&dev_cert_chain_tree, source);
} else {
code_sign_log_debug("add release cert");
ret = cert_chain_insert(&cert_chain_tree, source);
}
break;
case REMOVE_CERT_CHAIN:
if (code_sign_avc_has_perm(SECCLASS_CODE_SIGN, CODE_SIGN__REMOVE_CERT_CHAIN)) {
code_sign_log_error("selinux check failed, no permission to remove cert chain");
return -EPERM;
}
ret = parse_cert_source(args, &source);
if (ret)
return ret;
ret = code_sign_check_code(source->path_type);
if (ret < 0)
return ret;
if (ret == 1) {
code_sign_log_debug("remove developer cert");
ret = cert_chain_remove(&dev_cert_chain_tree, source);
} else {
code_sign_log_debug("remove release cert");
ret = cert_chain_remove(&cert_chain_tree, source);
}
if (ret) {
code_sign_log_error("remove cert failed.");
}
break;
default:
code_sign_log_error("code_sign cmd error, cmd: %d", cmd);
ret = -EINVAL;
break;
}
return ret;
}