Notes on a comprehensive analysis of the Intel SGX Local Attestation sample code. Warning: this post is horribly long and super dry.

Overview

The Linux SGX examples include a sample program for Local Attestation. It presents two Local Attestation designs: two enclaves within a single app, and two apps each with a corresponding enclave (here I only analyze the second design). Its Developer Reference, p95~, has a big map of LA.

LA Process

Intel’s official explanations of these steps can be found in the Developer Reference, p98, so I won’t repeat them here. Next I’ll analyze the implementation of LA.

Another document worth consulting is SGX 101 LA. But since the sample code it gives is probably from an SDK several versions ago, I’ll treat it only as a reference.

SGX Explained

sgx_dh_init_session

This function is called inside the enclave’s create_session function like this: status = sgx_dh_init_session(SGX_DH_SESSION_INITIATOR, &sgx_dh_session);. Here SGX_DH_SESSION_INITIATOR is one enum value of sgx_dh_session_role_t; the other is SGX_DH_SESSION_RESPONDER, representing the two kinds of roles in LA. sgx_dh_session is a struct of type sgx_dh_session_t, which is really just a simple uint8_t array — more on that later.

The Initiator calls this function. It’s declared in the sgx_dh.h header and implemented in the SDK’s 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;
}

The code above mainly initializes session: it zeroes out session’s memory and initializes state according to the role.

sgx_internal_dh_session_t in sgx_dh_internal.h

You can see that the sgx_dh_session passed in gets cast to sgx_internal_dh_session_t. Judging by the variable name, this is presumably the SDK’s internal representation of a DH session. It’s defined as:

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;

For an overview of DH, see wiki; I won’t repeat it here.

session_request_ocall

The call to it is status = session_request_ocall(&retstatus, &dh_msg1, &session_id);. One slightly misleading thing in the diagram is that this OCALL doesn’t actually call directly into AppResponder (Untrusted Code2); instead it first calls into session_request_ocall within the app.

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;
}

This function initializes some variables, then calls client_send_receive to send a msg1_request as the client and receive the msg1_response. This function does the send/receive over a socket.

Server Side

On the server side (AppResponder), the initial snippet looks like this:

        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;
                }
                // ....

Once it sees the header is FIFO_DH_REQ_MSG1, it kicks off the DH generate_and_send_session_msg1_resp flow. I won’t paste the full code; basically it calls session_request inside EnclaveResponder to generate the response, attaches the appropriate header, and sends the message back.

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;
}

On the responder side it also initializes the responder’s session. What stands out is the variable g_session_id_tracker, defined as session_id_tracker_t *g_session_id_tracker[MAX_SESSION_COUNT];. I suspect it’s meant to support the need for multiple LA sessions.

sgx_dh_responder_gen_msg1

After the session is established, it calls sgx_dh_responder_gen_msg1 to generate the actual msg1. And within that function it in turn calls dh_generate_message1, which does the real work of generating msg1.

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;
}

Here, a report belonging to the caller enclave is created by sgx_create_report and stored in temp_report. The odd thing here is that make_target_info happens after sgx_create_report. According to the specification of the EREPORT instruction, target_info should be provided as an input to it. Something fishy is going on here.

Only after reading further did I get it: here sgx_create_report generates a report about itself (the caller enclave), and then make_target_info extracts its own target info from that report. Comparing the fields of the two structs, it’s easy to see that the former contains all the information the latter needs.

Then, DH’s g_a (the public-key part of the generated keypair) is dumped into msg1. Other related info is also stored in context. The final msg1 is g_a||target_info.

Finally, within AppResponder, session_request returns with the msg1 that needs to be sent to the Initiator. This information is sent back to the Initiator.

sgx_dh_initiator_proc_msg1

The call to it is status = sgx_dh_initiator_proc_msg1(&dh_msg1, &dh_msg2, &sgx_dh_session);. sgx_dh_initiator_proc_msg1 is actually an alias for either sgx_LAv2_initiator_proc_msg1 or sgx_LAv1_initiator_proc_msg1, depending on whether LAv2 is defined.

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;
}

This function is really long. First it checks the validity of msg1, msg2, session, and role, then creates its own keypair, computes shared_key from g_a, and destroys its own priv_key; after that shared_key goes through one round of key derivation via derive_key, deriving dh_smk. Depending on the LA version (v1 or v2), msg2 is generated (see below). Finally, the session’s info — including pub_key, peer_pub_key, smk_aek, shared_key, and state — is all updated.

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);
}

Here the report data (rpt_data) is obtained by concatenating LAv2_proto_spec and *g_b and then hashing them; it will be stuffed into the subsequent report (rpt). This report is generated by calling EREPORT inside sgx_create_report, with &ti being the Responder’s target info, so that the generated report can (perhaps only) be opened by the Responder to verify the hash over rpt_data inside it (this prevents MITM).

After that, msg2 includes g_b along with the report just generated, which will be sent to the 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;
}

In v1, the hash stored in report_data is the hash of g_a || g_b. The tail of this hash is padded with AES_CMAC_KDF_ID. As in v2, the cmac is computed and packed into msg2.

exchange_report_ocall

Much like the earlier session_request_ocall, msg2 gets a header attached and is wrapped into a packet sent to the Responder, and then msg3 is received. For sanity’s sake I won’t paste the code here.

exchange_report

In the Responder’s server-side code (CPTask::run()), when it checks that header.type is FIFO_DH_MSG2, it calls the handler 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

I’m guessing this is the function that, inside EnclaveResponder, processes the received msg2 and then generates 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;
}

This code has a really strange idiom: do {} while(0);. In fact this while loop only runs once, so isn’t it pointless? Not quite — this way it can jump straight to below while(0); via break, without resorting to an ugly goto.

At the start it calls std::map<uint32_t, dh_session_t>::iterator. I’m honestly not entirely sure how this thing is used, but I’m guessing it finds the session that just received msg2.

Then it calls sgx_dh_responder_proc_msg2 to process msg2 and generate msg3 (see below).

If the previous step succeeds, it goes on to verify the peer enclave: verify_peer_enclave_trust. initiator_identity is generated during the processing of msg2. verify_peer_enclave_trust checks whether its mr_signer value matches what it (EnclaveResponder) has on record. (How is mr_signer obtained?)

According to Intel’s official statement, MRSIGNER is:

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;
}

Here, when defining dh_smk and shared_key in C++, & is used — a C++ feature (references). For details, see this article. Similar to sgx_dh_initiator_proc_msg1, after the routine checks on the input variables, it computes shared_key, clears responder.prv_key, and then derives the key.

Next it uses the v1 version of dh_verify_message2 (or LAv2_verify_message2 if that doesn’t succeed) to check msg2 (see below).

The platform-related info in msg2 gets dumped into initiator_identity. After that it starts generating msg3 (see below).

Then it derives the AEK from shared_key, and the session is officially established.

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;
}

It calls sgx_verify_report to verify rpt. First it finds the key_id in the report, then gets the corresponding report_key via sgx_get_key. It then MACs the report with this key and checks whether it matches the mac originally in the report — that completes the verification. After that it verifies msg2’s cmac (why does it also need to verify msg2’s?).

dh_verify_message2

The logic is similar, so I won’t paste the code.

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;
}

Similarly, the logic is much like that of generating msg2: the relevant info gets packed into a report. First ps (proto spec) is obtained from msg2’s report.body.report_data. Then the target info in msg3 is prepared by LAv2_proto_spec.make_target_info. Note that this make_target_info is actually not the same one as in msg1. All the fields in _target_info_t appear in _report_body_t, so it can be made.

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;
}

Similar to LAv2, so I won’t go into more detail.

sgx_dh_initiator_proc_msg3

This is also split into LAv1 and LAv2. Depending on the macro definition, the functions called are sgx_LAv2_initiator_proc_msg3 and sgx_LAv1_initiator_proc_msg3 respectively. Either way, the only difference is the function used to verify msg3; both use 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;
}

At the start, as usual, come the various in-enclave parameter checks. After that it does a ver_msg3 according to the LA version. On success, the EnclaveInitiator side also derives the AEK and clears the session info.

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;
}

This is the function used to verify msg3 in LAv2. It does almost exactly the same thing as the earlier EnclaveResponder did when verifying msg2: sgx_verify_report, then checking cmac consistency.

After story

After processing msg3, EnclaveInitiator, just like EnclaveResponder, also has to verify (verify_peer_enclave_trust) the responder_identity. Once that’s done, the session’s AEK is established and the session becomes active.

After this, you can start using this session’s key (AEK) to exchange messages. The message-exchange logic seems fairly simple, so if it’s warranted I might write another post about it.

Wrap-up

Studying the code in the SGX SDK carefully this time, you can tell it puts quite a bit of thought into security, and it’s written pretty cleanly.

Overall, the sample code in the SDK is genuinely very complex, and the process inside the SDK is quite involved. But the logic is still relatively clear: basically every function in the SDK goes through checking its parameters (non-null, all inside the enclave) and creating and zeroing the output buffers. Only after that does it get to the normal code-processing logic.