25 分钟读完

记一个对于Intel SGX Local Attestation Sample Code的一次Comprehensive的analysis。Warning:这篇文章十分臭长,超级干燥。

Overview

Linux SGX样例里面给了Local Attestation的程序Sample。这里给出了两个Local Attestation的design:一个App下的俩Enclave和俩App对应俩Enclave(这里只分析第二种设计)。从其Developer Reference, p95~, 中可以看到一个关于LA的big map。

LA Process

官方对于这些步骤的explanations可以在Developer Reference, p98,看到,此处不再赘述。接下来分析LA的Implementation。

另一份值得参考的文档是SGX 101 LA。但是由于其给出的Sample Code大概是好几个版本之前的SDK的,此处仅作参考。

SGX Explained

sgx_dh_init_session

该函数在Enclave的create_session函数里面被这调用:status = sgx_dh_init_session(SGX_DH_SESSION_INITIATOR, &sgx_dh_session);。其中 SGX_DH_SESSION_INITIATORsgx_dh_session_role_t的一个enum,另一个则是SGX_DH_SESSION_RESPONDER,代表LA中的两类roles。而sgx_dh_session是结构体sgx_dh_session_t,其实就是一个简单的unit8_t数组,在之后会更详细地介绍。

Initiator会调用这个函数,该函数在sgx_dh.h这个header里,实现在SDK里面的ec_dh_cpp中。

Implementation

sgx_status_t sgx_dh_init_session(sgx_dh_session_role_t role, sgx_dh_session_t* sgx_dh_session)
{
    sgx_internal_dh_session_t* session = (sgx_internal_dh_session_t*)sgx_dh_session;

    if(!session || 0 == sgx_is_within_enclave(session, sizeof(sgx_internal_dh_session_t)))
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(SGX_DH_SESSION_INITIATOR != role && SGX_DH_SESSION_RESPONDER != role)
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));

    if(SGX_DH_SESSION_INITIATOR == role)
    {
        session->initiator.state = SGX_DH_SESSION_INITIATOR_WAIT_M1;
    }
    else
    {
        session->responder.state = SGX_DH_SESSION_STATE_RESET;
    }

    session->role = role;

    return SGX_SUCCESS;
}

上面这段代码主要是对session初始化:清零了session的内存并根据role进行了state的初始化

sgx_internal_dh_session_t in sgx_dh_internal.h

可以看到传入的sgx_dh_session被强制转换成了sgx_internal_dh_session_t。根据变量名推测是SDK内部对于dh session的representation。其被定义为:

typedef struct _sgx_internal_dh_session_t{
    sgx_dh_session_role_t role;             /* Initiator or Responder */
    union{
        // this means an internal session is either an initiator or responder
        sgx_dh_responder_t responder;
        sgx_dh_initator_t  initiator;
    };
} sgx_internal_dh_session_t;

typedef struct _sgx_dh_responder_t{
    sgx_dh_session_state_t   state;	   /*Responder State Machine State */
    sgx_ec256_private_t prv_key;             /* 256bit EC private key */
    sgx_ec256_public_t  pub_key;             /* 512 bit EC public key */
} sgx_dh_responder_t;
 
typedef struct _sgx_dh_initator_t{
    sgx_dh_session_state_t state;    /* Initiator State Machine State */
    union{
        sgx_ec256_private_t prv_key;    /* 256bit EC private key */
        sgx_key_128bit_t smk_aek;    /* 128bit SMK or AEK. Depending on the State */
    };
    sgx_ec256_public_t pub_key;    /* 512 bit EC public key */
    sgx_ec256_public_t peer_pub_key;    /* 512 bit EC public key from the Responder */
    sgx_ec256_dh_shared_t shared_key;
} sgx_dh_initator_t;

DH的概述可以参考wiki,此处不再赘述。

session_request_ocall

对其的调用为status = session_request_ocall(&retstatus, &dh_msg1, &session_id);。这里图上有一点misleading的地方在于,其实这个ocall并没有直接call到AppResponder(Untrusted Code2),而是先call到了App内的session_request_ocall

session_request_ocall

/* Function Description: This is OCALL interface for initiator enclave to get ECDH message 1 and session id from responder enclave
 * Parameter Description:
 *      [input, output] dh_msg1: pointer to ecdh msg1 buffer, this buffer is allocated in initiator enclave and filled by responder enclave
 *      [output] session_id: pointer to session id which is allocated by responder enclave
 * */
extern "C" ATTESTATION_STATUS session_request_ocall(sgx_dh_msg1_t* dh_msg1, uint32_t* session_id)
{
	FIFO_MSG msg1_request;
	FIFO_MSG *msg1_response;
	SESSION_MSG1_RESP * msg1_respbody = NULL;
	size_t  msg1_resp_size;

	msg1_request.header.type = FIFO_DH_REQ_MSG1;
	msg1_request.header.size = 0;

	if ((client_send_receive(&msg1_request, sizeof(FIFO_MSG), &msg1_response, &msg1_resp_size) != 0)
		|| (msg1_response == NULL))
	{
		printf("fail to send and receive message.\n");
		return INVALID_SESSION;
	}

	msg1_respbody = (SESSION_MSG1_RESP *)msg1_response->msgbuf;
	memcpy(dh_msg1, &msg1_respbody->dh_msg1, sizeof(sgx_dh_msg1_t));
	*session_id = msg1_respbody->sessionid;
        free(msg1_response);

	return (ATTESTATION_STATUS)0;
}

在这个函数内初始化了一些变量,然后再调用client_send_receive以client发送一个msg1_request,并且receive msg1_response。这个函数是通过socket来完成收发的。

Server Side

在server端(AppResponder)里,一开始的snippet长这样:

        switch (message->header.type)
        {
            case FIFO_DH_REQ_MSG1:
            {
                // process ECDH session connection request
                int clientfd = message->header.sockfd;
            
                if (generate_and_send_session_msg1_resp(clientfd) != 0)
                {
                    printf("failed to generate and send session msg1 resp.\n");
                    break;
                }
                // ....

得知header是FIFO_DH_REQ_MSG1后就开始走DH的generate_and_send_session_msg1_resp流程了。完整的代码就不贴了,其主要就是调用EnclaveResponder内的session_request来生成response,配上相应的header,然后把消息回传。

session_request

//Handle the request from Source Enclave for a session
extern "C" ATTESTATION_STATUS session_request(sgx_dh_msg1_t *dh_msg1,
                          uint32_t *session_id )
{
    dh_session_t session_info;
    sgx_dh_session_t sgx_dh_session;
    sgx_status_t status = SGX_SUCCESS;

    if(!session_id || !dh_msg1)
    {
        return INVALID_PARAMETER_ERROR;
    }
    //Intialize the session as a session responder
    status = sgx_dh_init_session(SGX_DH_SESSION_RESPONDER, &sgx_dh_session);
    if(SGX_SUCCESS != status)
    {
        return status;
    }

    //get a new SessionID
    if ((status = (sgx_status_t)generate_session_id(session_id)) != SUCCESS)
        return status; //no more sessions available

    //Allocate memory for the session id tracker
    g_session_id_tracker[*session_id] = (session_id_tracker_t *)malloc(sizeof(session_id_tracker_t));
    if(!g_session_id_tracker[*session_id])
    {
        return MALLOC_ERROR;
    }

    memset(g_session_id_tracker[*session_id], 0, sizeof(session_id_tracker_t));
    g_session_id_tracker[*session_id]->session_id = *session_id;
    session_info.status = IN_PROGRESS;

    //Generate Message1 that will be returned to Source Enclave
    status = sgx_dh_responder_gen_msg1((sgx_dh_msg1_t*)dh_msg1, &sgx_dh_session);
    if(SGX_SUCCESS != status)
    {
        SAFE_FREE(g_session_id_tracker[*session_id]);
        return status;
    }
    memcpy(&session_info.in_progress.dh_session, &sgx_dh_session, sizeof(sgx_dh_session_t));
    //Store the session information under the corresponding source enclave id key
    g_dest_session_info_map.insert(std::pair<uint32_t, dh_session_t>(*session_id, session_info));

    return status;
}

在Responder side也会initialize responder的session。引人注意的是变量g_session_id_tracker,其定义为session_id_tracker_t *g_session_id_tracker[MAX_SESSION_COUNT];。怀疑是想要满足多个LA sessions的需求。

sgx_dh_responder_gen_msg1

在session建立之后其调用sgx_dh_responder_gen_msg1来生成真正的msg1。而在这个函数内又调用了真正用来生成msg1dh_generate_message1

dh_generate_message1

static sgx_status_t dh_generate_message1(sgx_dh_msg1_t *msg1, sgx_internal_dh_session_t *context)
{
    sgx_report_t temp_report;
    sgx_status_t se_ret;
    sgx_ecc_state_handle_t ecc_state = NULL;

    if(!msg1 || !context)
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    //Create Report to get target info which targeted towards the initiator of the session
    se_ret = sgx_create_report(nullptr, nullptr, &temp_report);
    if(se_ret != SGX_SUCCESS)
    {
        return se_ret;
    }

    if (SGX_SUCCESS != (se_ret =
        LAv2_proto_spec.make_target_info(temp_report, msg1->target)))
    {
        return se_ret;
    }

    //Initialize ECC context to prepare for creating key pair
    se_ret = sgx_ecc256_open_context(&ecc_state);
    if(se_ret != SGX_SUCCESS)
    {
        return se_ret;
    }
    //Generate the public key private key pair for Session Responder
    se_ret = sgx_ecc256_create_key_pair((sgx_ec256_private_t*)&context->responder.prv_key,
                                       (sgx_ec256_public_t*)&context->responder.pub_key,
                                       ecc_state);
    if(se_ret != SGX_SUCCESS)
    {
         sgx_ecc256_close_context(ecc_state);
         return se_ret;
    }

    //Copying public key to g^a
    memcpy(&msg1->g_a,
           &context->responder.pub_key,
           sizeof(sgx_ec256_public_t));

    se_ret = sgx_ecc256_close_context(ecc_state);
    if(SGX_SUCCESS != se_ret)
    {
        return se_ret;
    }

    return SGX_SUCCESS;
}

在这里,属于caller enclaved的report被sgx_create_report创建并存储在temp_report。这里奇怪的一点是make_target_info发生在sgx_create_report之后。而按照EREPORT指令的specificationtarget_info应该作为input输入其中。此处有蹊跷

看了后面我才明白,这里sgx_create_report生成了关于自己(caller enclave)的一个report,然后通过make_target_info导出了report中自己的target info。根据两个结构体的字段不难发现前者包含后者所需要的所有信息。

然后,dh中的g_a(被生成出来的keypair中的public ket的部分)被丢在了msg1内。相关的其他信息也被存储在了context内。最终的msg1g_a||target_info

最后,在AppResponder内,session_request返回时带上了需要发送给Initiator的msg1。这些信息被发回Initiator

sgx_dh_initiator_proc_msg1

对其的调用在status = sgx_dh_initiator_proc_msg1(&dh_msg1, &dh_msg2, &sgx_dh_session);。而sgx_dh_initiator_proc_msg1其实是sgx_LAv2_initiator_proc_msg1或者sgx_LAv1_initiator_proc_msg1的alias。取决于有没有define LAv2

template <decltype(dh_generate_message2) gen_msg2>
static sgx_status_t dh_initiator_proc_msg1(const sgx_dh_msg1_t* msg1, sgx_dh_msg2_t* msg2, sgx_dh_session_t* sgx_dh_session)
{
    sgx_status_t se_ret;

    sgx_ec256_public_t pub_key;
    sgx_ec256_private_t priv_key;
    sgx_ec256_dh_shared_t shared_key;
    sgx_key_128bit_t dh_smk;

    sgx_internal_dh_session_t* session = (sgx_internal_dh_session_t*) sgx_dh_session;

    /* ROUTINE CHECKS BEGIN */

    // validate session
    if(!session ||
        0 == sgx_is_within_enclave(session, sizeof(sgx_internal_dh_session_t))) // session must be in enclave
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if( !msg1 ||
        !msg2 ||
        0 == sgx_is_within_enclave(msg1, sizeof(sgx_dh_msg1_t)) ||
        0 == sgx_is_within_enclave(msg2, sizeof(sgx_dh_msg2_t)) ||
        SGX_DH_SESSION_INITIATOR != session->role)
    {
        // clear secret when encounter error
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(SGX_DH_SESSION_INITIATOR_WAIT_M1 != session->initiator.state)
    {
        // clear secret
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_STATE;
    }

    /* ROUTINE CHECKS END */

    //create ECC context
    sgx_ecc_state_handle_t ecc_state = NULL;
    se_ret = sgx_ecc256_open_context(&ecc_state);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }
    // generate private key and public key
    se_ret = sgx_ecc256_create_key_pair((sgx_ec256_private_t*)&priv_key,
                                       (sgx_ec256_public_t*)&pub_key,
                                        ecc_state);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    //generate shared_key
    se_ret = sgx_ecc256_compute_shared_dhkey(
                                            (sgx_ec256_private_t *)const_cast<sgx_ec256_private_t*>(&priv_key),
                                            (sgx_ec256_public_t *)const_cast<sgx_ec256_public_t*>(&msg1->g_a),
                                            (sgx_ec256_dh_shared_t *)&shared_key,
                                             ecc_state);

    // clear private key for defense in depth
    memset_s(&priv_key, sizeof(sgx_ec256_private_t), 0, sizeof(sgx_ec256_private_t));

    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    se_ret = derive_key(&shared_key, "SMK", (uint32_t)(sizeof("SMK") -1), &dh_smk);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    se_ret = gen_msg2(msg1, &pub_key, &dh_smk, msg2);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    memcpy(&session->initiator.pub_key, &pub_key, sizeof(sgx_ec256_public_t));
    memcpy(&session->initiator.peer_pub_key, &msg1->g_a, sizeof(sgx_ec256_public_t));
    memcpy(&session->initiator.smk_aek, &dh_smk, sizeof(sgx_key_128bit_t));
    memcpy(&session->initiator.shared_key, &shared_key, sizeof(sgx_ec256_dh_shared_t));
    // clear shared key and SMK
    memset_s(&shared_key, sizeof(sgx_ec256_dh_shared_t), 0, sizeof(sgx_ec256_dh_shared_t));
    memset_s(&dh_smk, sizeof(sgx_key_128bit_t), 0, sizeof(sgx_key_128bit_t));

    if(SGX_SUCCESS != sgx_ecc256_close_context(ecc_state))
    {
        // clear session
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        // set error state
        session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_UNEXPECTED;
    }

    session->initiator.state = SGX_DH_SESSION_INITIATOR_WAIT_M3;
    return SGX_SUCCESS;

error:
    sgx_ecc256_close_context(ecc_state);

    // clear shared key and SMK
    memset_s(&shared_key, sizeof(sgx_ec256_dh_shared_t), 0, sizeof(sgx_ec256_dh_shared_t));
    memset_s(&dh_smk, sizeof(sgx_key_128bit_t), 0, sizeof(sgx_key_128bit_t));

    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
    session->initiator.state = SGX_DH_SESSION_STATE_ERROR;

    // return selected error to upper layer
    INTERNAL_SGX_ERROR_CODE_CONVERTOR(se_ret)

    return se_ret;
}

这个函数是真的长啊。一开始检查了msg1, msg2, session, role的validity,然后创建了自己的keypair,根据g_a计算出来shared_key,销毁自己的priv_key;之后shared_keyderive_key做了一次key derivation, derive出来了dh_smk。根据LA的版本不同(v1 or v2),msg2被generate (see below), 。最后,session的信息,包括pub_key, peer_pub_key, smk_aek, shared_key, state都被更新。

LAv1 and LAv2

Generator caller

//sgx_LAv1_initiator_proc_msg1 processes M1 message, generates M2 message and makes update to the context of the session.
sgx_status_t sgx_LAv1_initiator_proc_msg1(const sgx_dh_msg1_t* msg1,
    sgx_dh_msg2_t* msg2, sgx_dh_session_t* sgx_dh_session)
{
    return dh_initiator_proc_msg1<dh_generate_message2>(msg1, msg2, sgx_dh_session);
}

// sgx_LAv2_initiator_proc_msg1() is a drop-in replacement of sgx_dh_initiator_proc_msg1()
sgx_status_t SGXAPI sgx_LAv2_initiator_proc_msg1(
    const sgx_dh_msg1_t* msg1, sgx_dh_msg2_t* msg2, sgx_dh_session_t* sgx_dh_session)
{
    return dh_initiator_proc_msg1<LAv2_generate_message2>(msg1, msg2, sgx_dh_session);
}

msg2 Generator

LAv2

static sgx_status_t LAv2_generate_message2(
    const sgx_dh_msg1_t *msg1, const sgx_ec256_public_t *g_b,
    const sgx_key_128bit_t *dh_smk, sgx_dh_msg2_t *msg2)
{
    // No need to validate input parameters in an internal static function
    sgx_report_data_t rpt_data =  0;
    bufcat(LAv2_proto_spec, *g_b).sha256(&rpt_data);

    sgx_target_info_t ti(msg1->target);
    sgx_report_t rpt;
    sgx_create_report(&ti, &rpt_data, &rpt);

    // Replace report_data with proto_spec
    rpt.body.report_data = LAv2_proto_spec.cast_to_report_data();

    // Put together LAv2 Message 2
    msg2->g_b = *g_b;
    msg2->report = rpt;
    return sgx_rijndael128_cmac_msg(dh_smk,
        reinterpret_cast<const uint8_t*>(&msg2->g_b), sizeof(msg2->g_b), &msg2->cmac);
}

这里report data(rpt_data)是把LAv2_proto_spec*g_bconcatenate起来之后再hash得到的,将会被塞入之后的report(rpt)之中。而这个report通过sgx_create_report里面调用EREPORT生成,&ti是Responder的target info,这样生成出来的report可以(或许只能)被Responder解开并验证里面关于rpt_data的hash(这样可以防止MITM)。

之后,msg2里面囊括了g_b以及刚刚生成出来的report,这将被发给Responder。

LAv1

static sgx_status_t dh_generate_message2(const sgx_dh_msg1_t *msg1,
                                         const sgx_ec256_public_t *g_b,
                                         const sgx_key_128bit_t *dh_smk,
                                         sgx_dh_msg2_t *msg2)
{
    sgx_report_t temp_report;
    sgx_report_data_t report_data;

    sgx_status_t se_ret;

    uint8_t msg_buf[MSG_BUF_LEN] = {0};
    uint8_t msg_hash[MSG_HASH_SZ] = {0};

    if(!msg1 || !g_b || !dh_smk || !msg2)
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    memset(msg2, 0, sizeof(sgx_dh_msg2_t));
    memcpy(&msg2->g_b, g_b, sizeof(sgx_ec256_public_t));

    memcpy(msg_buf,
           &msg1->g_a,
           sizeof(sgx_ec256_public_t));
    memcpy(msg_buf + sizeof(sgx_ec256_public_t),
           &msg2->g_b,
           sizeof(sgx_ec256_public_t));

    se_ret = sgx_sha256_msg(msg_buf,
                            MSG_BUF_LEN,
                            (sgx_sha256_hash_t *)msg_hash);
    if(SGX_SUCCESS != se_ret)
    {
        return se_ret;
    }

    // Get REPORT with sha256(msg1->g_a | msg2->g_b) || kdf_id as user data
    // 2-byte little-endian KDF-ID: 0x0001 AES-CMAC Entropy Extraction and Key Derivation
    memset(&report_data, 0, sizeof(sgx_report_data_t));
    memcpy(&report_data, &msg_hash, sizeof(msg_hash));

    uint16_t *kdf_id = (uint16_t *)&report_data.d[sizeof(msg_hash)];
    *kdf_id = AES_CMAC_KDF_ID;

    // Generate Report targeted towards Session Responder
    se_ret = sgx_create_report(&msg1->target, &report_data, &temp_report);
    if(SGX_SUCCESS != se_ret)
    {
        return se_ret;
    }

    memcpy(&msg2->report, &temp_report, sizeof(sgx_report_t));

    //Calculate the MAC for Message 2
    se_ret = sgx_rijndael128_cmac_msg(dh_smk,
                                      (uint8_t *)(&msg2->report),
                                      sizeof(sgx_report_t),
                                      (sgx_cmac_128bit_tag_t *)msg2->cmac);
    if(SGX_SUCCESS != se_ret)
    {
        return se_ret;
    }

    return SGX_SUCCESS;
}

在v1中,report_data内存储的hash是g_a || g_b的hash。这个hash的尾部被AES_CMAC_KDF_ID进行填充。和v2一样,cmac会被计算出来装进msg2

exchange_report_ocall

和之前的session_request_ocall差不多,msg2被加上header封装成包发给Responder,然后再接收msg3。这里for sanity就不贴代码了。

exchange_report

在Responder的server side代码中(CPTask::run()),检查header.typeFIFO_DH_MSG2时调用处理函数process_exchange_report:

            case FIFO_DH_MSG2:
            {
                // process ECDH message 2
                int clientfd = message->header.sockfd;
                SESSION_MSG2 * msg2 = NULL;
                msg2 = (SESSION_MSG2 *)message->msgbuf;

                if (process_exchange_report(clientfd, msg2) != 0)
                {
                    printf("failed to process exchange_report request.\n");
                    break;
                }
            }

process_exchange_report

int process_exchange_report(int clientfd, SESSION_MSG2 * msg2)
{
    uint32_t status = 0;
        sgx_status_t ret = SGX_SUCCESS;
    FIFO_MSG *response;
    SESSION_MSG3 * msg3;
    size_t msgsize;
    
    if (!msg2)
        return -1;
    
    msgsize = sizeof(FIFO_MSG_HEADER) + sizeof(SESSION_MSG3);
    response = (FIFO_MSG *)malloc(msgsize);
    if (!response)
    {
        printf("memory allocation failure\n");
        return -1;
    }
    memset(response, 0, msgsize);
    
    response->header.type = FIFO_DH_MSG3;
    response->header.size = sizeof(SESSION_MSG3);
    
    msg3 = (SESSION_MSG3 *)response->msgbuf;
    msg3->sessionid = msg2->sessionid; 

    // call responder enclave to process ECDH message 2 and generate message 3
    ret = exchange_report(e2_enclave_id, &status, &msg2->dh_msg2, &msg3->dh_msg3, msg2->sessionid);
    if (ret != SGX_SUCCESS)
    {
        printf("EnclaveResponse_exchange_report failure.\n");
        free(response);
        return -1;
    }

    // send ECDH message 3 to client
    if (send(clientfd, reinterpret_cast<char *>(response), static_cast<int>(msgsize), 0) == -1)
    {
        printf("server_send() failure.\n");
        free(response);
        return -1;
    }

    free(response);

    return 0;
}

exchange_report

猜测是在EnclaveResponder内处理收到的msg2之后generate msg3的函数。

//Verify Message 2, generate Message3 and exchange Message 3 with Source Enclave
extern "C" ATTESTATION_STATUS exchange_report(sgx_dh_msg2_t *dh_msg2,
                          sgx_dh_msg3_t *dh_msg3,
                          uint32_t session_id)
{

    sgx_key_128bit_t dh_aek;   // Session key
    dh_session_t *session_info;
    ATTESTATION_STATUS status = SUCCESS;
    sgx_dh_session_t sgx_dh_session;
    sgx_dh_session_enclave_identity_t initiator_identity;

    if(!dh_msg2 || !dh_msg3)
    {
        return INVALID_PARAMETER_ERROR;
    }

    memset(&dh_aek,0, sizeof(sgx_key_128bit_t));
    do
    {
        //Retrieve the session information for the corresponding source enclave id
        std::map<uint32_t, dh_session_t>::iterator it = g_dest_session_info_map.find(session_id);
        if(it != g_dest_session_info_map.end())
        {
            session_info = &it->second;
        }
        else
        {
            status = INVALID_SESSION;
            break;
        }

        if(session_info->status != IN_PROGRESS)
        {
            status = INVALID_SESSION;
            break;
        }

        memcpy(&sgx_dh_session, &session_info->in_progress.dh_session, sizeof(sgx_dh_session_t));

        dh_msg3->msg3_body.additional_prop_length = 0;
        //Process message 2 from source enclave and obtain message 3
        sgx_status_t se_ret = sgx_dh_responder_proc_msg2(dh_msg2,
                                                       dh_msg3,
                                                       &sgx_dh_session,
                                                       &dh_aek,
                                                       &initiator_identity);
        if(SGX_SUCCESS != se_ret)
        {
            status = se_ret;
            break;
        }

        //Verify source enclave's trust
          if(verify_peer_enclave_trust(&initiator_identity) != SUCCESS)
        {
            return INVALID_SESSION;
        }

        //save the session ID, status and initialize the session nonce
        session_info->session_id = session_id;
        session_info->status = ACTIVE;
        session_info->active.counter = 0;
        memcpy(session_info->active.AEK, &dh_aek, sizeof(sgx_key_128bit_t));
        memset(&dh_aek,0, sizeof(sgx_key_128bit_t));
        g_session_count++;
    }while(0);

    if(status != SUCCESS)
    {
        end_session(session_id);
    }

    return status;
}

这个代码有一个非常奇怪的用法do {} while(0);。实际上这个while循环只会跑一次,那么岂不是没有什么卵用?其实不然,这样它就可以通过break直接跳到while(0);下面而不需要使用丑陋的goto了。

一开始调用了std::map<uint32_t, dh_session_t>::iterator。实际上这玩意究竟是怎么用的我并不是很清楚,但是猜测是找到了一个现在接收到msg2的session。

然后调用sgx_dh_responder_proc_msg2处理msg2并生成msg3(see below)。

上一步如果成功,会接着验证peer enclave:verify_peer_enclave_trustinitiator_identity在处理msg2的过程中被生成。verify_peer_enclave_trust会check其mr_signer的值是否与自己(EnclaveResponder)所记录的一致。(mr_signer是怎么得到的?)

根据官方的说法,MRSIGNER是:

The second is the Signing Identity provided by an authority, which signs the enclave prior to distribution. This value is called MRSIGNER and will be the same for all enclaves signed with the same authority.

sgx_dh_responder_proc_msg2

/*Function name: sgx_dh_responder_proc_msg2
** parameter description
**@ [input] msg2: point to dh message 2 buffer generated by session initiator, and the buffer must be in enclave address space
**@ [output] msg3: point to dh message 3 buffer generated by session responder in this function, and the buffer must be in enclave address space
**@ [input/output] dh_session: point to dh session structure that is used during establishment, and the buffer must be in enclave address space
**@ [output] aek: AEK derived from shared key. the buffer must be in enclave address space.
**@ [output] initiator_identity: identity information of initiator including isv svn, isv product id, sgx attributes, mr signer, and mr enclave. the buffer must be in enclave address space.
*/
//sgx_dh_responder_proc_msg2 processes M2 message, generates M3 message, and returns the session key AEK.
sgx_status_t sgx_dh_responder_proc_msg2(const sgx_dh_msg2_t* msg2,
                                        sgx_dh_msg3_t* msg3,
                                        sgx_dh_session_t* sgx_dh_session,
                                        sgx_key_128bit_t* aek,
                                        sgx_dh_session_enclave_identity_t* initiator_identity)
{
    sgx_status_t se_ret;

    //
    // securely align shared key
    //
    //sgx_ec256_dh_shared_t shared_key;
    sgx::custom_alignment<sgx_ec256_dh_shared_t, 0, sizeof(sgx_ec256_dh_shared_t)> oshared_key;
    sgx_ec256_dh_shared_t& shared_key = oshared_key.v;
    //
    // securely align smk
    //
    //sgx_key_128bit_t dh_smk;
    sgx::custom_alignment_aligned<sgx_key_128bit_t, sizeof(sgx_key_128bit_t), 0, sizeof(sgx_key_128bit_t)> odh_smk;
    sgx_key_128bit_t& dh_smk = odh_smk.v;

    sgx_internal_dh_session_t* session = (sgx_internal_dh_session_t*)sgx_dh_session;
    bool is_LAv2 = false;

    // validate session
    if(!session ||
        0 == sgx_is_within_enclave(session, sizeof(sgx_internal_dh_session_t))) // session must be in enclave
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(!msg3 ||
        msg3->msg3_body.additional_prop_length > (UINT_MAX - sizeof(sgx_dh_msg3_t)) || // check msg3 length overflow
        0 == sgx_is_within_enclave(msg3, (sizeof(sgx_dh_msg3_t)+msg3->msg3_body.additional_prop_length)) || // must be in enclave
        !msg2 ||
        0 == sgx_is_within_enclave(msg2, sizeof(sgx_dh_msg2_t)) || // must be in enclave
        !aek ||
        0 == sgx_is_within_enclave(aek, sizeof(sgx_key_128bit_t)) || // must be in enclave
        !initiator_identity ||
        0 == sgx_is_within_enclave(initiator_identity, sizeof(sgx_dh_session_enclave_identity_t)) || // must be in enclave
        SGX_DH_SESSION_RESPONDER != session->role)
    {
        // clear secret when encounter error
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->responder.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(SGX_DH_SESSION_RESPONDER_WAIT_M2 != session->responder.state) // protocol state must be SGX_DH_SESSION_RESPONDER_WAIT_M2
    {
        // clear secret
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->responder.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_STATE;
    }

    //create ECC context, and the ECC parameter is
    //NIST standard P-256 elliptic curve.
    sgx_ecc_state_handle_t ecc_state = NULL;
    se_ret = sgx_ecc256_open_context(&ecc_state);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    //generate shared key, which should be identical with enclave side,
    //from PSE private key and enclave public key
    se_ret = sgx_ecc256_compute_shared_dhkey((sgx_ec256_private_t *)&session->responder.prv_key,
                                            (sgx_ec256_public_t *)const_cast<sgx_ec256_public_t*>(&msg2->g_b),
                                            (sgx_ec256_dh_shared_t *)&shared_key,
                                             ecc_state);

    // For defense-in-depth purpose, responder clears its private key from its enclave memory, as it's not needed anymore.
    memset_s(&session->responder.prv_key, sizeof(sgx_ec256_private_t), 0, sizeof(sgx_ec256_private_t));

    if(se_ret != SGX_SUCCESS)
    {
        goto error;
    }


    //derive keys from session shared key
    se_ret = derive_key(&shared_key, "SMK", (uint32_t)(sizeof("SMK") -1), &dh_smk);
    if(se_ret != SGX_SUCCESS)
    {
        goto error;
    }
    // Verify message 2 from Session Initiator and also Session Initiator's identity
    se_ret = dh_verify_message2(msg2, &session->responder.pub_key, &dh_smk);
    if(SGX_SUCCESS != se_ret &&
       !(is_LAv2 = LAv2_verify_message2(msg2, &dh_smk)))
    {
        goto error;
    }

    initiator_identity->isv_svn = msg2->report.body.isv_svn;
    initiator_identity->isv_prod_id = msg2->report.body.isv_prod_id;
    memcpy(&initiator_identity->attributes, &msg2->report.body.attributes, sizeof(sgx_attributes_t));
    memcpy(&initiator_identity->mr_signer, &msg2->report.body.mr_signer, sizeof(sgx_measurement_t));
    memcpy(&initiator_identity->mr_enclave, &msg2->report.body.mr_enclave, sizeof(sgx_measurement_t));

    // Generate message 3 to send back to initiator
    se_ret = is_LAv2 ?
        LAv2_generate_message3(msg2, &session->responder.pub_key, &dh_smk, msg3) :
        dh_generate_message3(msg2, &session->responder.pub_key, &dh_smk, msg3,
                               msg3->msg3_body.additional_prop_length);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    // derive session key
    se_ret = derive_key(&shared_key, "AEK", (uint32_t)(sizeof("AEK") -1), aek);
    if(se_ret != SGX_SUCCESS)
    {
        goto error;
    }

    // clear secret
    memset_s(&shared_key, sizeof(sgx_ec256_dh_shared_t), 0, sizeof(sgx_ec256_dh_shared_t));
    memset_s(&dh_smk, sizeof(sgx_key_128bit_t), 0, sizeof(sgx_key_128bit_t));
    // clear session
    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));

    se_ret = sgx_ecc256_close_context(ecc_state);
    if(SGX_SUCCESS != se_ret)
    {
        // set error state
        session->responder.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_UNEXPECTED;
    }

    // set state
    session->responder.state = SGX_DH_SESSION_ACTIVE;

    return SGX_SUCCESS;

error:
    sgx_ecc256_close_context(ecc_state);
    // clear secret
    memset_s(&shared_key, sizeof(sgx_ec256_dh_shared_t), 0, sizeof(sgx_ec256_dh_shared_t));
    memset_s(&dh_smk, sizeof(sgx_key_128bit_t), 0, sizeof(sgx_key_128bit_t));
    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
    // set error state
    session->responder.state = SGX_DH_SESSION_STATE_ERROR;
    // return selected error to upper layer
    if (se_ret != SGX_ERROR_OUT_OF_MEMORY &&
        se_ret != SGX_ERROR_KDF_MISMATCH)
    {
        se_ret = SGX_ERROR_UNEXPECTED;
    }
    return se_ret;
}

这里在C++中定义dh_smkshared_key的时候用到了&,是C++中的一个feature。具体可以参考这篇文章。与sgx_dh_initiator_proc_msg1 类似,进行输入变量的例行检查之后,计算出来shared_key,清空responder.prv_key。再derive key。

接下来便用v1(if not success, v2)版本的dh_verify_message2(LAv2_verify_message2)来检查msg2。(see below)。

msg2中关于platform的信息会被丢进initiator_identity中。之后便开始生成msg3(see below)。

之后便通过shared_keyderive出AEK,session被正式建立。

msg2 verification

LAv2_verify_message2

static bool LAv2_verify_message2(const sgx_dh_msg2_t *msg2, const sgx_key_128bit_t *dh_smk)
{
    auto rpt = msg2->report;
    rpt.body.report_data = 0;
    bufcat(msg2->report.body.report_data, msg2->g_b).sha256(&rpt.body.report_data);
    if (SGX_SUCCESS != sgx_verify_report(&rpt))
        return false;

    if (SGX_SUCCESS != verify_cmac128(*dh_smk,
        reinterpret_cast<const uint8_t*>(&msg2->g_b), sizeof(msg2->g_b), msg2->cmac))
        return false;

    return memcmp(&msg2->report.body.report_data, &LAv2_proto_spec,
        offsetof(decltype(LAv2_proto_spec), rev)) == 0;
}

调用sgx_verify_report来verify rpt。先找到report中的key_id,然后通过sgx_get_key拿到对应的report_key。再用这个key对report做mac查看是否和原本report中的mac一致即完成验证。之后再验证msg2的cmac(为什么还要验证msg2的呢?)。

dh_verify_message2

逻辑类似,不贴代码了。

msg3 generation

LAv2_generate_message3

static sgx_status_t LAv2_generate_message3(const sgx_dh_msg2_t *msg2,
    const sgx_ec256_public_t *A, const sgx_key_128bit_t *dh_smk, sgx_dh_msg3_t *msg3)
{
    sgx_status_t se_ret;
    auto& ps = LAv2_proto_spec.cast_from(msg2->report.body.report_data);

    sgx_target_info_t ti;
    if (SGX_SUCCESS != (se_ret =
        LAv2_proto_spec.make_target_info(ps, msg2->report, ti)))
        return se_ret;

    sgx_report_data_t rpt_data = 0;
    bufcat(*A, ps).sha256(&rpt_data);

    sgx_report_t rpt;
    sgx_create_report(&ti, &rpt_data, &rpt);
    msg3->msg3_body.report = rpt;

    sgx_cmac_state_handle_t cmac;
    if (SGX_SUCCESS != (se_ret = sgx_cmac128_init(dh_smk, &cmac)))
        return se_ret;

    sgx_cmac128_update(msg3->msg3_body.additional_prop,
        msg3->msg3_body.additional_prop_length, cmac);
    sgx_cmac128_update(reinterpret_cast<const uint8_t*>(A), sizeof(*A), cmac);
    sgx_cmac128_final(cmac, &msg3->cmac);
    sgx_cmac128_close(cmac);

    return SGX_SUCCESS;
}

类似的,和msg2生成的逻辑差不多,相关的信息被打包进了一个report。首先ps(proto spec)是从msg2report.body.report_data中拿到的。然后msg3中的target info由LAv2_proto_spec.make_target_info准备好。注意这里的make_target_infomsg1中函数其实不是同一个。_target_info_t中的所有字段都在_report_body_t中出现,故而能够make出来。

LAv1 dh_generate_message3

static sgx_status_t dh_generate_message3(const sgx_dh_msg2_t *msg2,
                                         const sgx_ec256_public_t *g_a,
                                         const sgx_key_128bit_t *dh_smk,
                                         sgx_dh_msg3_t *msg3,
                                         uint32_t msg3_additional_prop_len)
{
    sgx_report_t temp_report;
    sgx_report_data_t report_data;
    sgx_status_t se_ret = SGX_SUCCESS;
    uint32_t maced_size;

    uint8_t msg_buf[MSG_BUF_LEN] = {0};
    uint8_t msg_hash[MSG_HASH_SZ] = {0};

    sgx_target_info_t target;

    if(!msg2 || !g_a || !dh_smk || !msg3)
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    maced_size = static_cast<uint32_t>(sizeof(sgx_dh_msg3_body_t)) + msg3_additional_prop_len;

    memset(msg3, 0, sizeof(sgx_dh_msg3_t)); // Don't clear the additional property since the content of the property is provided by caller.

    memcpy(msg_buf, &msg2->g_b, sizeof(sgx_ec256_public_t));
    memcpy(msg_buf + sizeof(sgx_ec256_public_t), g_a, sizeof(sgx_ec256_public_t));

    se_ret = sgx_sha256_msg(msg_buf,
                            MSG_BUF_LEN,
                            (sgx_sha256_hash_t *)msg_hash);
    if(se_ret != SGX_SUCCESS)
    {
        return se_ret;
    }

    // Get REPORT with SHA256(g_b||g_a) as user data
    memset(&report_data, 0, sizeof(sgx_report_data_t));
    memcpy(&report_data, &msg_hash, sizeof(msg_hash));

    if (SGX_SUCCESS != (se_ret =
        LAv2_proto_spec.make_target_info(msg2->report, target)))
    {
        return se_ret;
    }

    // Generate Report targeted towards Session Initiator
    se_ret = sgx_create_report(&target, &report_data, &temp_report);
    if(se_ret != SGX_SUCCESS)
    {
        return se_ret;
    }

    memcpy(&msg3->msg3_body.report,
           &temp_report,
           sizeof(sgx_report_t));

    msg3->msg3_body.additional_prop_length = msg3_additional_prop_len;

    //Calculate the MAC for Message 3
    se_ret = sgx_rijndael128_cmac_msg(dh_smk,
                                      (uint8_t *)&msg3->msg3_body,
                                      maced_size,
                                      (sgx_cmac_128bit_tag_t *)msg3->cmac);
    if(se_ret != SGX_SUCCESS)
    {
        return se_ret;
    }

    return SGX_SUCCESS;
}

和LAv2类似,不多赘述。

sgx_dh_initiator_proc_msg3

也是分为LAv1和LAv2。根据宏定义不同调用的函数分别为sgx_LAv2_initiator_proc_msg3sgx_LAv1_initiator_proc_msg3。不论如何,区别仅仅在于调用的用来verify msg3的函数不同,其都使用了dh_initiator_proc_msg3

LAv2

/*Function name: sgx_LAv2_initiator_proc_msg3
** parameter description
**@ [input] msg3: point to dh message 3 buffer generated by session responder, and the buffer must be in enclave address space
**@ [input/output] dh_session: point to dh session structure that is used during establishment, and the buffer must be in enclave address space
**@ [output] aek: AEK derived from shared key. the buffer must be in enclave address space.
**@ [output] responder_identity: identity information of responder including isv svn, isv product id, sgx attributes, mr signer, and mr enclave. the buffer must be in enclave address space.
*/
sgx_status_t sgx_LAv2_initiator_proc_msg3(const sgx_dh_msg3_t* msg3,
    sgx_dh_session_t* sgx_dh_session, sgx_key_128bit_t* aek,
    sgx_dh_session_enclave_identity_t* responder_identity)
{
    return dh_initiator_proc_msg3<LAv2_verify_message3>(
        msg3, sgx_dh_session, aek, responder_identity);
}

LAv1

/*Function name: sgx_LAv1_initiator_proc_msg3
** parameter description
**@ [input] msg3: point to dh message 3 buffer generated by session responder, and the buffer must be in enclave address space
**@ [input/output] dh_session: point to dh session structure that is used during establishment, and the buffer must be in enclave address space
**@ [output] aek: AEK derived from shared key. the buffer must be in enclave address space.
**@ [output] responder_identity: identity information of responder including isv svn, isv product id, sgx attributes, mr signer, and mr enclave. the buffer must be in enclave address space.
*/
sgx_status_t sgx_LAv1_initiator_proc_msg3(const sgx_dh_msg3_t* msg3,
    sgx_dh_session_t* sgx_dh_session, sgx_key_128bit_t* aek,
    sgx_dh_session_enclave_identity_t* responder_identity)
{
    return dh_initiator_proc_msg3<dh_verify_message3>(
        msg3, sgx_dh_session, aek, responder_identity);
}

`dh_initiator_proc_msg3

template <decltype(dh_verify_message3) ver_msg3>
static sgx_status_t dh_initiator_proc_msg3(const sgx_dh_msg3_t* msg3,
    sgx_dh_session_t* sgx_dh_session, sgx_key_128bit_t* aek,
    sgx_dh_session_enclave_identity_t* responder_identity)
{
    sgx_status_t se_ret;
    sgx_internal_dh_session_t* session = (sgx_internal_dh_session_t*)sgx_dh_session;

    // validate session
    if(!session ||
        0 == sgx_is_within_enclave(session, sizeof(sgx_internal_dh_session_t))) // session must be in enclave
    {
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(!msg3 ||
        msg3->msg3_body.additional_prop_length > (UINT_MAX - sizeof(sgx_dh_msg3_t)) || // check msg3 length overflow
        0 == sgx_is_within_enclave(msg3, (sizeof(sgx_dh_msg3_t)+msg3->msg3_body.additional_prop_length)) || // msg3 buffer must be in enclave
        !aek ||
        0 == sgx_is_within_enclave(aek, sizeof(sgx_key_128bit_t)) || // aek buffer must be in enclave
        !responder_identity ||
        0 == sgx_is_within_enclave(responder_identity, sizeof(sgx_dh_session_enclave_identity_t)) || // responder_identity buffer must be in enclave
        SGX_DH_SESSION_INITIATOR != session->role) // role must be SGX_DH_SESSION_INITIATOR
    {
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_PARAMETER;
    }

    if(SGX_DH_SESSION_INITIATOR_WAIT_M3 != session->initiator.state) // protocol state must be SGX_DH_SESSION_INITIATOR_WAIT_M3
    {
        memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
        session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
        return SGX_ERROR_INVALID_STATE;
    }

    se_ret = ver_msg3(msg3, &session->initiator.peer_pub_key,
        &session->initiator.pub_key, &session->initiator.smk_aek);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    // derive AEK
    se_ret = derive_key(&session->initiator.shared_key, "AEK", (uint32_t)(sizeof("AEK") -1), aek);
    if(SGX_SUCCESS != se_ret)
    {
        goto error;
    }

    // clear session
    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
    session->initiator.state = SGX_DH_SESSION_ACTIVE;

    // copy the common fields between REPORT and the responder enclave identity
    memcpy(responder_identity, &msg3->msg3_body.report.body, sizeof(sgx_dh_session_enclave_identity_t));

    return SGX_SUCCESS;

error:
    memset_s(session, sizeof(sgx_internal_dh_session_t), 0, sizeof(sgx_internal_dh_session_t));
    session->initiator.state = SGX_DH_SESSION_STATE_ERROR;
    INTERNAL_SGX_ERROR_CODE_CONVERTOR(se_ret)
    return se_ret;
}

一开始还是照常的Enclave内的各种参数检查。这之后会根据LA的version来做一个ver_msg3。成功之后EnclaveInitiator side也会derive AEK,清除session信息。

LAv2_verify_message3

static sgx_status_t LAv2_verify_message3(const sgx_dh_msg3_t *msg3,
    const sgx_ec256_public_t *A, const sgx_ec256_public_t *, const sgx_key_128bit_t *dh_smk)
{
    auto rpt = msg3->msg3_body.report;
    rpt.body.report_data = 0;
    bufcat(*A, LAv2_proto_spec).sha256(&rpt.body.report_data);
    if (memcmp(&msg3->msg3_body.report.body.report_data,
            &rpt.body.report_data, sizeof(rpt.body.report_data)) ||
        SGX_SUCCESS != sgx_verify_report(&rpt))
        return SGX_ERROR_UNEXPECTED;

    sgx_cmac_state_handle_t cmac;
    if (SGX_SUCCESS != sgx_cmac128_init(dh_smk, &cmac))
        return SGX_ERROR_OUT_OF_MEMORY;

    sgx_cmac_128bit_tag_t tag;
    sgx_cmac128_update(msg3->msg3_body.additional_prop,
        msg3->msg3_body.additional_prop_length, cmac);
    sgx_cmac128_update(reinterpret_cast<const uint8_t*>(A), sizeof(*A), cmac);
    sgx_cmac128_final(cmac, &tag);
    sgx_cmac128_close(cmac);

    return consttime_memequal(&tag, msg3->cmac, sizeof(tag)) ?
        SGX_SUCCESS : SGX_ERROR_MAC_MISMATCH;
}

在LAv2内用来verify msg3的函数。与之前的EnclaveResponder在verify msg2时几乎做的是同样的事情:sgx_verify_report,检查cmac一致性。

After story

EnclaveInitiator在处理完msg3之后,和EnclaveResponder一样,也要验证一下(verify_peer_enclave_trust) responder_identity。完成之后整个session的AEK被建立起来,session变成active。

这之后便可以开始使用这个session的密钥(AEK)来交换消息了。消息交互的逻辑似乎比较简单,如果有必要的话我说不定会再写一篇。

总结

这次仔细研读SGX SDK里面的代码可以看出来其对于安全的考量还是比较多的,写的也相当规范。

总体而言,SDK里面的Sample Code是真的很复杂,SDK里面的过程也很繁琐。但是逻辑还是相对比较清晰的,基本上SDK里面的每一个函数都要经历:对参数的检查(不为空,全部在enclave里面),对于输出buffer的创建和清零。这之后才会经历正常的代码处理逻辑。

留下评论