接上一节的分析,我们在这里详细的分析DEMO中CA和TA的源码,整个分析过程以捋清楚整个流程为主,在整个过程中,涉及的点很多,后面章节会再一一详解,如optee os的启动、driver的注册、以及守护进程tee_supplicant。
源码git地址:https://github.com/4LogCoder/optee.git
在ca中,我主要封装了三个函数,分别是:
TEEC_Result ca_demo_sha_open(void);
TEEC_Result ca_demo_sha(void* in, unsigned int size, void *out, SHA_MODE mode);
void ca_demo_sha_close(void);
在main函数中,我们首先调用ca_demo_sha_open()函数,建立和打开一个session,建立CA和TA的联系,然后通过ca_demo_sha()函数发起一次SHA计算。
首先我们先从ca_demo_open开始
TEEC_Result ca_demo_sha_open(void)
{
TEEC_Result ret;
TEEC_UUID id = TA_DEMO_UUID;
//printf("ca_demo_sha_open\n",ret);
ret = TEEC_InitializeContext(NULL, &context);
if(ret != TEEC_SUCCESS){
printf("ca_demo_sha_open:TEEC_InitializeContext failed %x\n",ret);
goto exit_1;
}
ret = TEEC_OpenSession(&context, &session, &id, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
if(ret != TEEC_SUCCESS){
printf("ca_demo_sha_open:TEEC_OpenSession failed %x\n",ret);
goto finalizeContext;
}
sharedMem.size = 1024 + 1024;
sharedMem.flags = TEEC_MEM_INPUT | TEEC_MEM_OUTPUT;
ret = TEEC_AllocateSharedMemory(&context, &sharedMem);
if(ret != TEEC_SUCCESS){
printf("ca_demo_sha_open:TEEC_AllocateSharedMemory failed %x\n",ret);
goto closeSession;
}
valid_session = 1;
return ret;
closeSession:
TEEC_CloseSession(&session);
finalizeContext:
TEEC_FinalizeContext(&context);
exit_1:
return ret;
}
在该函数中,我们依次调用了
TEEC_InitializeContext(NULL, &context);
TEEC_OpenSession(&context, &session, &id, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
TEEC_AllocateSharedMemory(&context, &sharedMem);
这三个函数均由libteec库实现,下面我们一一分析这三个函数都干了些什么
首先是调用TEEC_InitializeContext函数
函数原型:TEEC_Result TEEC_InitializeContext(const char *name, TEEC_Context *ctx)
功能:初始化一个TEEC_Context变量
详解:在这函数中,首先拼接出devname字符串,然后调用teec_open_dev()函数,这个函数则主要是通过调用open()函数,打开对应的device,并通过ioctl获取版本等相关信息,进行相应的判断后,将打开的device的句柄fd放入TEEC_Context变量之中。
TEEC_Result TEEC_InitializeContext(const char *name, TEEC_Context *ctx)
{
char devname[PATH_MAX] = { 0 };
int fd = 0;
size_t n = 0;
if (!ctx)
return TEEC_ERROR_BAD_PARAMETERS;
for (n = 0; n < TEEC_MAX_DEV_SEQ; n++) {
uint32_t gen_caps = 0;
snprintf(devname, sizeof(devname), "/dev/tee%zu", n);
fd = teec_open_dev(devname, name, &gen_caps);
if (fd >= 0) {
ctx->fd = fd;
ctx->reg_mem = gen_caps & TEE_GEN_CAP_REG_MEM;
return TEEC_SUCCESS;
}
}
return TEEC_ERROR_ITEM_NOT_FOUND;
}
static int teec_open_dev(const char *devname, const char *capabilities,
uint32_t *gen_caps)
{
int fd = 0;
struct tee_ioctl_version_data vers;
memset(&vers, 0, sizeof(vers));
fd = open(devname, O_RDWR);
if (fd < 0)
return -1;
if (ioctl(fd, TEE_IOC_VERSION, &vers)) {
EMSG("TEE_IOC_VERSION failed");
goto err;
}
/* We can only handle GP TEEs */
if (!(vers.gen_caps & TEE_GEN_CAP_GP))
goto err;
if (capabilities) {
if (strcmp(capabilities, "optee-tz") == 0) {
if (vers.impl_id != TEE_IMPL_ID_OPTEE)
goto err;
if (!(vers.impl_caps & TEE_OPTEE_CAP_TZ))
goto err;
} else {
/* Unrecognized capability requested */
goto err;
}
}
*gen_caps = vers.gen_caps;
return fd;
err:
close(fd);
return -1;
}
TEEC_InitializeContext函数小结:这一函数主要是通过打开对应的设备文件,获取对应设备文件的fd,并进行版本检查,将fd填充至context变量。在整个过程中,并没有和某个特定的TA联系。此外这一函数必须是CA调用libteec库的第一个API,因为只用通过这一函数,才打开了对应的设备文件,和optee_linuxdriver建立了联系。
调用TEEC_InitializeContext函数之后,再调用TEEC_OpenSession()函数
函数原型:
TEEC_Result TEEC_OpenSession(TEEC_Context *ctx, TEEC_Session *session,
const TEEC_UUID *destination,
uint32_t connection_method, const void *connection_data,
TEEC_Operation *operation, uint32_t *ret_origin)
函数功能:在当前context中为CA和TA建立一个session,具体和那个TA建立session由TEEC_UUID变量决定
详解:在这一函数中,首先定义了一块sharememory用于存放ioctl的相关参数,并进行相应的预处理,最终通过调用ioctl发送TEE_IOC_OPEN_SESSION command,陷入正常世界的内核空间,到达tee driver中
TEEC_Result TEEC_OpenSession(TEEC_Context *ctx, TEEC_Session *session,
const TEEC_UUID *destination,
uint32_t connection_method, const void *connection_data,
TEEC_Operation *operation, uint32_t *ret_origin)
{
struct tee_ioctl_open_session_arg *arg = NULL;
struct tee_ioctl_param *params = NULL;
TEEC_Result res = TEEC_ERROR_GENERIC;
uint32_t eorig = 0;
int rc = 0;
const size_t arg_size = sizeof(struct tee_ioctl_open_session_arg) +
TEEC_CONFIG_PAYLOAD_REF_COUNT *
sizeof(struct tee_ioctl_param);
union {
struct tee_ioctl_open_session_arg arg;
uint8_t data[arg_size];
} buf;
struct tee_ioctl_buf_data buf_data;
TEEC_SharedMemory shm[TEEC_CONFIG_PAYLOAD_REF_COUNT];
memset(&buf, 0, sizeof(buf));
memset(&shm, 0, sizeof(shm));
memset(&buf_data, 0, sizeof(buf_data));
(void)&connection_data;
if (!ctx || !session) {
eorig = TEEC_ORIGIN_API;
res = TEEC_ERROR_BAD_PARAMETERS;
goto out;
}
buf_data.buf_ptr = (uintptr_t)&buf;
buf_data.buf_len = sizeof(buf);
arg = &buf.arg;
arg->num_params = TEEC_CONFIG_PAYLOAD_REF_COUNT;
params = (struct tee_ioctl_param *)(arg + 1);
uuid_to_octets(arg->uuid, destination);
arg->clnt_login = connection_method;
res = teec_pre_process_operation(ctx, operation, params, shm);
if (res != TEEC_SUCCESS) {
eorig = TEEC_ORIGIN_API;
goto out_free_temp_refs;
}
rc = ioctl(ctx->fd, TEE_IOC_OPEN_SESSION, &buf_data);
if (rc) {
EMSG("TEE_IOC_OPEN_SESSION failed");
eorig = TEEC_ORIGIN_COMMS;
res = ioctl_errno_to_res(errno);
goto out_free_temp_refs;
}
res = arg->ret;
eorig = arg->ret_origin;
if (res == TEEC_SUCCESS) {
session->ctx = ctx;
session->session_id = arg->session;
}
teec_post_process_operation(operation, params, shm);
out_free_temp_refs:
teec_free_temp_refs(operation, shm);
out:
if (ret_origin)
*ret_origin = eorig;
return res;
}
下面我们看一下ioctl的TEE_IOC_OPEN_SESSION command具体干了些啥
源码位置: tee/core.c
首先通过tee_ioctl调用tee_ioctl_open_session()函数,在该函数中,首先将用户空间的参数拷贝到正常世界的内核空间
static int tee_ioctl_open_session(struct tee_context *ctx,
struct tee_ioctl_buf_data __user *ubuf)
{
int rc;
size_t n;
struct tee_ioctl_buf_data buf;
struct tee_ioctl_open_session_arg __user *uarg;
struct tee_ioctl_open_session_arg arg;
struct tee_ioctl_param __user *uparams = NULL;
struct tee_param *params = NULL;
bool have_session = false;
if (!ctx->teedev->desc->ops->open_session)
return -EINVAL;
if (copy_from_user(&buf, ubuf, sizeof(buf)))
return -EFAULT;
if (buf.buf_len > TEE_MAX_ARG_SIZE ||
buf.buf_len < sizeof(struct tee_ioctl_open_session_arg))
return -EINVAL;
uarg = u64_to_user_ptr(buf.buf_ptr);
if (copy_from_user(&arg, uarg, sizeof(arg)))
return -EFAULT;
if (sizeof(arg) + TEE_IOCTL_PARAM_SIZE(arg.num_params) != buf.buf_len)
return -EINVAL;
if (arg.num_params) {
params = kcalloc(arg.num_params, sizeof(struct tee_param),
GFP_KERNEL);
if (!params)
return -ENOMEM;
uparams = uarg->params;
rc = params_from_user(ctx, params, arg.num_params, uparams);
if (rc)
goto out;
}
rc = ctx->teedev->desc->ops->open_session(ctx, &arg, params);
if (rc)
goto out;
have_session = true;
if (put_user(arg.session, &uarg->session) ||
put_user(arg.ret, &uarg->ret) ||
put_user(arg.ret_origin, &uarg->ret_origin)) {
rc = -EFAULT;
goto out;
}
rc = params_to_user(uparams, arg.num_params, params);
out:
/*
* If we've succeeded to open the session but failed to communicate
* it back to user space, close the session again to avoid leakage.
*/
if (rc && have_session && ctx->teedev->desc->ops->close_session)
ctx->teedev->desc->ops->close_session(ctx, arg.session);
if (params) {
/* Decrease ref count for all valid shared memory pointers */
for (n = 0; n < arg.num_params; n++)
if (tee_param_is_memref(params + n) &&
params[n].u.memref.shm)
tee_shm_put(params[n].u.memref.shm);
kfree(params);
}
return rc;
}
然后调用fops:的open_session变量,即optee_open_session函数,在optee_open_session函数中,通过get_msg_arg分配一块sharememory,并将参数填充至sharememory中,最后调用optee_do_call_with_arg函数,在这一函数中,通过optee->invoke_fn触发smc调用。
int optee_open_session(struct tee_context *ctx,
struct tee_ioctl_open_session_arg *arg,
struct tee_param *param)
{
struct optee_context_data *ctxdata = ctx->data;
int rc;
struct tee_shm *shm;
struct optee_msg_arg *msg_arg;
phys_addr_t msg_parg;
struct optee_session *sess = NULL;
/* +2 for the meta parameters added below */
shm = get_msg_arg(ctx, arg->num_params + 2, &msg_arg, &msg_parg);
if (IS_ERR(shm))
return PTR_ERR(shm);
msg_arg->cmd = OPTEE_MSG_CMD_OPEN_SESSION;
msg_arg->cancel_id = arg->cancel_id;
/*
* Initialize and add the meta parameters needed when opening a
* session.
*/
msg_arg->params[0].attr = OPTEE_MSG_ATTR_TYPE_VALUE_INPUT |
OPTEE_MSG_ATTR_META;
msg_arg->params[1].attr = OPTEE_MSG_ATTR_TYPE_VALUE_INPUT |
OPTEE_MSG_ATTR_META;
memcpy(&msg_arg->params[0].u.value, arg->uuid, sizeof(arg->uuid));
memcpy(&msg_arg->params[1].u.value, arg->uuid, sizeof(arg->clnt_uuid));
msg_arg->params[1].u.value.c = arg->clnt_login;
rc = optee_to_msg_param(msg_arg->params + 2, arg->num_params, param);
if (rc)
goto out;
sess = kzalloc(sizeof(*sess), GFP_KERNEL);
if (!sess) {
rc = -ENOMEM;
goto out;
}
if (optee_do_call_with_arg(ctx, msg_parg)) {
msg_arg->ret = TEEC_ERROR_COMMUNICATION;
msg_arg->ret_origin = TEEC_ORIGIN_COMMS;
}
if (msg_arg->ret == TEEC_SUCCESS) {
/* A new session has been created, add it to the list. */
sess->session_id = msg_arg->session;
mutex_lock(&ctxdata->mutex);
list_add(&sess->list_node, &ctxdata->sess_list);
mutex_unlock(&ctxdata->mutex);
} else {
kfree(sess);
}
if (optee_from_msg_param(param, arg->num_params, msg_arg->params + 2)) {
arg->ret = TEEC_ERROR_COMMUNICATION;
arg->ret_origin = TEEC_ORIGIN_COMMS;
/* Close session again to avoid leakage */
optee_close_session(ctx, msg_arg->session);
} else {
arg->session = msg_arg->session;
arg->ret = msg_arg->ret;
arg->ret_origin = msg_arg->ret_origin;
}
out:
tee_shm_free(shm);
return rc;
}
在optee_do_call_with_arg函数中,通过invoke_fn函数触发smc调用
u32 optee_do_call_with_arg(struct tee_context *ctx, phys_addr_t parg)
{
struct optee *optee = tee_get_drvdata(ctx->teedev);
struct optee_call_waiter w;
struct optee_rpc_param param = { };
struct optee_call_ctx call_ctx = { };
u32 ret;
param.a0 = OPTEE_SMC_CALL_WITH_ARG;
reg_pair_from_64(¶m.a1, ¶m.a2, parg);
/* Initialize waiter */
optee_cq_wait_init(&optee->call_queue, &w);
while (true) {
struct arm_smccc_res res;
optee->invoke_fn(param.a0, param.a1, param.a2, param.a3,
param.a4, param.a5, param.a6, param.a7,
&res);
if (res.a0 == OPTEE_SMC_RETURN_ETHREAD_LIMIT) {
/*
* Out of threads in secure world, wait for a thread
* become available.
*/
optee_cq_wait_for_completion(&optee->call_queue, &w);
} else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) {
param.a0 = res.a0;
param.a1 = res.a1;
param.a2 = res.a2;
param.a3 = res.a3;
optee_handle_rpc(ctx, ¶m, &call_ctx);
} else {
ret = res.a0;
break;
}
}
optee_rpc_finalize_call(&call_ctx);
/*
* We're done with our thread in secure world, if there's any
* thread waiters wake up one.
*/
optee_cq_wait_final(&optee->call_queue, &w);
return ret;
}
在这里具体是怎么触发smc调用的,先大致介绍下,后面章节会详细讲解。这个函数指针是通过get_invoke_func函数遍历设备树,获取相应的smc调用方式
/* Simple wrapper functions to be able to use a function pointer */
static void optee_smccc_smc(unsigned long a0, unsigned long a1,
unsigned long a2, unsigned long a3,
unsigned long a4, unsigned long a5,
unsigned long a6, unsigned long a7,
struct arm_smccc_res *res)
{
arm_smccc_smc(a0, a1, a2, a3, a4, a5, a6, a7, res);
}
static void optee_smccc_hvc(unsigned long a0, unsigned long a1,
unsigned long a2, unsigned long a3,
unsigned long a4, unsigned long a5,
unsigned long a6, unsigned long a7,
struct arm_smccc_res *res)
{
arm_smccc_hvc(a0, a1, a2, a3, a4, a5, a6, a7, res);
}
static optee_invoke_fn *get_invoke_func(struct device_node *np)
{
const char *method;
pr_info("probing for conduit method from DT.\n");
if (of_property_read_string(np, "method", &method)) {
pr_warn("missing \"method\" property\n");
return ERR_PTR(-ENXIO);
}
if (!strcmp("hvc", method))
return optee_smccc_hvc;
else if (!strcmp("smc", method))
return optee_smccc_smc;
pr_warn("invalid \"method\" property: %s\n", method);
return ERR_PTR(-EINVAL);
}
触发smc调用之后,处理器就进入Monitor模式,并将一系列的寄存器值更新,获取MVBAR寄存器中异常向量表中的sm_vect_table的基地址,并命中smc异常,从而进入sm_smc_entry函数。函数位置:optee_os-master\core\arch\arm\sm\sm_a32.S,然后更新一些寄存器值,最后依次调用smc_from_nsec -> sm_from_nsec
uint32_t sm_from_nsec(struct sm_ctx *ctx)
{
uint32_t *nsec_r0 = (uint32_t *)(&ctx->nsec.r0);
/*
* Check that struct sm_ctx has the different parts properly
* aligned since the stack pointer will be updated to point at
* different parts of this struct.
*/
COMPILE_TIME_ASSERT(!(offsetof(struct sm_ctx, sec.r0) % 8));
COMPILE_TIME_ASSERT(!(offsetof(struct sm_ctx, nsec.r0) % 8));
COMPILE_TIME_ASSERT(!(sizeof(struct sm_ctx) % 8));
#ifdef CFG_SM_PLATFORM_HANDLER
if (sm_platform_handler(ctx) == SM_HANDLER_SMC_HANDLED)
return SM_EXIT_TO_NON_SECURE;
#endif
#ifdef CFG_PSCI_ARM32
if (OPTEE_SMC_OWNER_NUM(*nsec_r0) == OPTEE_SMC_OWNER_STANDARD) {
smc_std_handler((struct thread_smc_args *)nsec_r0, &ctx->nsec);
return SM_EXIT_TO_NON_SECURE;
}
#endif
sm_save_unbanked_regs(&ctx->nsec.ub_regs);
sm_restore_unbanked_regs(&ctx->sec.ub_regs);
memcpy(&ctx->sec.r0, nsec_r0, sizeof(uint32_t) * 8);
if (OPTEE_SMC_IS_FAST_CALL(ctx->sec.r0))
ctx->sec.mon_lr = (uint32_t)&thread_vector_table.fast_smc_entry;
else
ctx->sec.mon_lr = (uint32_t)&thread_vector_table.std_smc_entry;
return SM_EXIT_TO_SECURE;
}
在sm_from_nsec函数中,通过判断,最后进入std_smc_entry,调用entry_open_session()处理,最后通过一系列的调用,直到调用到ta_load()函数。
static TEE_Result ta_load(struct tee_tadb_ta_read *ta)
{
TEE_Result res;
const size_t sz = ta->entry.prop.custom_size + ta->entry.prop.bin_size;
if (ta->ta_mobj)
return TEE_SUCCESS;
ta->ta_mobj = thread_rpc_alloc_payload(sz);
if (!ta->ta_mobj)
return TEE_ERROR_OUT_OF_MEMORY;
ta->ta_buf = mobj_get_va(ta->ta_mobj, 0);
assert(ta->ta_buf);
struct thread_param params[] = {
[0] = THREAD_PARAM_VALUE(IN, OPTEE_RPC_FS_READ, ta->fd, 0),
[1] = THREAD_PARAM_MEMREF(OUT, ta->ta_mobj, 0, sz),
};
res = thread_rpc_cmd(OPTEE_RPC_CMD_FS, ARRAY_SIZE(params), params);
if (res) {
thread_rpc_free_payload(ta->ta_mobj);
ta->ta_mobj = NULL;
}
return res;
}
在ta_load函数中,主要通过调用thread_rpc_cmd函数,再在thread_rpc_cmd函数中调用thread_rpc函数,发起RPC请求,其中thread_rpc由汇编实现,通过smc指令,将系统从secure world转换至Normal World
FUNC thread_smc , :
smc #0
ret
END_FUNC thread_smc
在linux kernel tee driver中,driver将会通知supplicant完成TA文件的加载(后面章节中,再详细讲解这一过程)。
至此,我们已经通过调用TEEC_OpenSession完成了相应TA文件的加载。
TEEC_OpenSession小结:在这一函数中,我们通过ioctl调用,陷入内核空间,然后调用smc指令,从NormalWorld转换至SecureWorld,转换至secureWorld后,由optee os解析command,准备load相应的TA文件,在这一步骤中,还需要调用smc指令,转换至NormalWorld,请求tee_supplicat进程在文件系统中找到对应UUID的TA文件,并传至secureWorld中。至此TEEC_OpenSession函数才全部执行完成。
此后,我们再调用AllocateSharedMemory函数,分配一块shareMemory,存放待计算的明文。
通过上述三个函数的准备工作之后,我们再在main函数中,调用ca_demo_sha()函数,发起一次计算请求。
TEEC_Result ca_demo_sha(void* in, unsigned int size, void *out, SHA_MODE mode)
{
TEEC_Result ret;
TEEC_Operation op;
uint32_t err_origin;
//printf("ca_demo_sha\n");
if(!valid_session)
return TEEC_ERROR_BAD_STATE;
if(size > 1024)
return TEEC_ERROR_BAD_PARAMETERS;
memcpy(sharedMem.buffer, in, size);
op.paramTypes = TEEC_PARAM_TYPES(TEEC_VALUE_INPUT, TEEC_MEMREF_PARTIAL_INPUT,
TEEC_MEMREF_PARTIAL_OUTPUT, TEEC_NONE);
op.started = 1;
op.params[0].value.a = mode;
op.params[1].memref.parent = &sharedMem;
op.params[1].memref.offset = 0;
op.params[1].memref.size = size;
op.params[2].memref.parent = &sharedMem;
op.params[2].memref.offset = 1024;
op.params[2].memref.size = 1024;
ret = TEEC_InvokeCommand(&session, CA_DEMO_CMD_SHA, &op, &err_origin);
if(ret != TEEC_SUCCESS){
printf("invoke faild %d\n",ret);
return ret;
}
memcpy(out, (void*)((char *)sharedMem.buffer + 1024), op.params[2].memref.size);
uint32_t len = op.params[2].memref.size;
printf("ca_demo_sha: len:%d sha:\n", len);
int i;
char* p = out;
for(i= 0; i
在这一过程中,我们首先需要构建填充好相应的参数(具体可参考官方文档),然后调用TEEC_InvokeCommand函数,正式发起一次计算command。
整个过程和TEEC_InvokeCommand类似,主要是通过ioctl陷入内核,然后调用smc指令,转换至secureWorld,并由optee os解析指令,最终调用到TA文件中的TA_InvokeCommandEntryPoint函数
TEE_Result TA_InvokeCommandEntryPoint(void *pSessionContext,uint32_t nCommandID,
uint32_t nParamTypes, TEE_Param pParams[4])
至此,就开始执行用户设定的一些code.
TEE_Result TA_InvokeCommandEntryPoint(void *pSessionContext,uint32_t nCommandID,
uint32_t nParamTypes, TEE_Param pParams[4])
{
DMSG("TA Invoke Command Entry Point for %s commandId: %d\n",TA_NAME,nCommandID);
switch(nCommandID){
case TA_DEMO_CMD_SHA:
return ta_demo_entry_sha(nParamTypes, pParams);
case TA_DEMO_CMD_RSA:
return ta_demo_entry_rsa(nParamTypes, pParams);
default:
return TEE_ERROR_BAD_PARAMETERS;
}
}
在此计算完成之后,就会通过类似于RPC请求的方式,将结果返回值NormalWorld.
然后再在main函数中,调用ca_demo_close()函数,释放相应的session和设备。在这里就不再详细介绍了。
总结下,整个demo主要就是封装了ca_demo_sha_open(),ca_demo_sha(),ca_demo_sha_close,首先在ca_demo_sha_oopen函数中,调用了TEEC_InitializeContext、TEEC_OpenSession、TEEC_AllocateSharedMemory三个函数,打开了相应的tee设备文件,并将指定的TA load进了内存之中,并和当前CA进行了关联;然后通过在ca_demo_sha()中调用TEEC_InvokeCommand函数,最终调用到TA文件中的TEE_Result TA_InvokeCommandEntryPoint(void *pSessionContext,uint32_t nCommandID, uint32_t nParamTypes, TEE_Param pParams[4])函数,在这一函数中执行相应的SHA操作,并将计算结果放在sharememory中,返回至NormalWorld中。