core/cmd_interface/session_manager.c (401 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "cmd_interface.h"
#include "session_manager.h"
#include "common/buffer_util.h"
#include "common/common_math.h"
#include "crypto/hash.h"
#include "crypto/kdf.h"
#include "riot/riot_core.h"
#include "riot/riot_key_manager.h"
/**
* Search session manager's active sessions table and find entry for requested EID if it exists.
*
* @param session Session manager instance to utilize.
* @param eid Requested EID for device in session.
*
* @return Requested session container if exists, NULL otherwise.
*/
struct session_manager_entry* session_manager_get_session (struct session_manager *session,
uint8_t eid)
{
size_t i_session;
for (i_session = 0; i_session < session->num_sessions; ++i_session) {
if ((session->sessions_table[i_session].eid == eid) &&
(session->sessions_table[i_session].session_state != SESSION_STATE_UNUSED)) {
return &session->sessions_table[i_session];
}
}
return NULL;
}
/**
* Search session manager's sessions table and find first unused entry.
*
* @param session Session manager instance to utilize.
*
* @return Free session container if exists, NULL otherwise.
*/
struct session_manager_entry* session_manager_get_free_session (struct session_manager *session)
{
size_t i_session;
for (i_session = 0; i_session < session->num_sessions; ++i_session) {
if (session->sessions_table[i_session].session_state == SESSION_STATE_UNUSED) {
return &session->sessions_table[i_session];
}
}
return NULL;
}
/**
* Search session manager's pairing devices EID list and find keystore index for requested EID if it
* exists.
*
* @param session Session manager instance to utilize.
* @param eid Requested EID for device in session.
*
* @return Requested keystore index if exists, or an error code.
*/
static int session_manager_get_paired_key_index (struct session_manager *session, uint8_t eid)
{
size_t i_key;
for (i_key = 0; i_key < session->num_pairing_eids; ++i_key) {
if (session->pairing_eids[i_key] == eid) {
return i_key;
}
}
return SESSION_MANAGER_PAIRING_NOT_SUPPORTED_WITH_DEVICE;
}
/**
* Find AES session key for requested EID then set it in the AES engine
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
* @param entry points to requested session container if exists
*
* @return Completion status, 0 if success or an error code.
*/
static int session_manager_set_key (struct session_manager *session, uint8_t eid,
struct session_manager_entry **entry)
{
struct session_manager_entry *curr_session;
int status;
curr_session = session_manager_get_session (session, eid);
if (curr_session == NULL) {
return SESSION_MANAGER_UNEXPECTED_EID;
}
else if (curr_session->session_state < SESSION_STATE_ESTABLISHED) {
return SESSION_MANAGER_SESSION_NOT_ESTABLISHED;
}
status = session->aes->set_key (session->aes, curr_session->session_key,
sizeof (curr_session->session_key));
if (entry) {
*entry = curr_session;
}
return status;
}
/**
* Check if device EID is on an established session.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
*
* @return 1 if established, 0 if not or an error code.
*/
int session_manager_is_session_established (struct session_manager *session, uint8_t eid)
{
struct session_manager_entry *req_session;
if (session == NULL) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
req_session = session_manager_get_session (session, eid);
if (req_session == NULL) {
return false;
}
return (req_session->session_state >= SESSION_STATE_ESTABLISHED);
}
/**
* Check pairing state of session with device.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
*
* @return Pairing state or an error code.
*/
int session_manager_get_pairing_state (struct session_manager *session, uint8_t eid)
{
struct session_manager_entry *req_session;
uint8_t *pairing_key_buf;
size_t pairing_key_len;
int key_id;
int status;
if (session == NULL) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
key_id = session_manager_get_paired_key_index (session, eid);
if (ROT_IS_ERROR (key_id) || (session->store == NULL)) {
return SESSION_PAIRING_STATE_NOT_SUPPORTED;
}
status = session->store->load_key (session->store, key_id, &pairing_key_buf, &pairing_key_len);
if (status == KEYSTORE_NO_KEY) {
return SESSION_PAIRING_STATE_NOT_INITIALIZED;
}
else if (status != 0) {
return status;
}
riot_core_clear (pairing_key_buf, pairing_key_len);
platform_free (pairing_key_buf);
req_session = session_manager_get_session (session, eid);
if ((req_session == NULL) || (req_session->session_state != SESSION_STATE_PAIRED)) {
return SESSION_PAIRING_STATE_NOT_PAIRED;
}
return SESSION_PAIRING_STATE_PAIRED;
}
/**
* Decrypt message using AES session key generated for session with device with requested EID.
*
* @param session Session manager instance to utilize.
* @param request Request to decrypt.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_decrypt_message (struct session_manager *session,
struct cmd_interface_msg *request)
{
uint8_t *payload;
size_t payload_len;
size_t buffer_len;
int status;
if ((session == NULL) || (request == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
if (request->length <= (SESSION_MANAGER_TRAILER_LEN +
sizeof (struct cerberus_protocol_header))) {
return SESSION_MANAGER_MALFORMED_MSG;
}
if ((request->max_response < (request->length - SESSION_MANAGER_TRAILER_LEN)) ||
(request->max_response <=
(sizeof (struct cerberus_protocol_header) + SESSION_MANAGER_TRAILER_LEN))) {
return SESSION_MANAGER_BUF_TOO_SMALL;
}
payload = request->data + CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID;
payload_len = request->length - CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID -
SESSION_MANAGER_TRAILER_LEN;
buffer_len = request->max_response - CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID;
status = session_manager_set_key (session, request->source_eid, NULL);
if (status != 0) {
return status;
}
request->length -= SESSION_MANAGER_TRAILER_LEN;
return session->aes->decrypt_data (session->aes, payload, payload_len, &payload[payload_len],
&payload[payload_len + CERBERUS_PROTOCOL_AES_GCM_TAG_LEN], CERBERUS_PROTOCOL_AES_IV_LEN,
payload, buffer_len);
}
/**
* Encrypt message using AES session key generated for session with device with requested EID.
*
* @param session Session manager instance to utilize.
* @param request Request to encrypt.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_encrypt_message (struct session_manager *session,
struct cmd_interface_msg *request)
{
struct cerberus_protocol_header *header;
uint8_t *aes_iv;
uint8_t *payload;
size_t payload_len;
size_t buffer_len;
int status;
struct session_manager_entry *curr_session;
if ((session == NULL) || (request == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
if (request->length < sizeof (struct cerberus_protocol_header)) {
return 0;
}
if ((request->length + SESSION_MANAGER_TRAILER_LEN) > request->max_response) {
return SESSION_MANAGER_BUF_TOO_SMALL;
}
payload = request->data + CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID;
payload_len = request->length - CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID;
buffer_len = request->max_response - CERBERUS_PROTOCOL_HEADER_SIZE_NO_ID;
aes_iv = &payload[payload_len + CERBERUS_PROTOCOL_AES_GCM_TAG_LEN];
status = session_manager_set_key (session, request->source_eid, &curr_session);
if (status != 0) {
return status;
}
status = common_math_increment_byte_array (curr_session->aes_init_vector,
CERBERUS_PROTOCOL_AES_IV_LEN, false);
if (status != 0) {
return status;
}
memcpy (aes_iv, curr_session->aes_init_vector, CERBERUS_PROTOCOL_AES_IV_LEN);
status = session->aes->encrypt_data (session->aes, payload, payload_len, aes_iv,
CERBERUS_PROTOCOL_AES_IV_LEN, payload, buffer_len - SESSION_MANAGER_TRAILER_LEN,
&payload[payload_len], CERBERUS_PROTOCOL_AES_GCM_TAG_LEN);
if (status != 0) {
return status;
}
request->length += SESSION_MANAGER_TRAILER_LEN;
header = (struct cerberus_protocol_header*) request->data;
header->crypt = 1;
return status;
}
/**
* Use provided nonces to either create or restart session with device specified using EID.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
* @param device_nonce 32 byte random nonce generated by device used for AES key generation.
* @param cerberus_nonce 32 byte random nonce generated by Cerberus used for AES key generation.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_add_session (struct session_manager *session, uint8_t eid,
const uint8_t *device_nonce, const uint8_t *cerberus_nonce)
{
struct session_manager_entry *curr_session;
if ((session == NULL) || (device_nonce == NULL) || (cerberus_nonce == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
curr_session = session_manager_get_session (session, eid);
if (curr_session == NULL) {
curr_session = session_manager_get_free_session (session);
if (curr_session == NULL) {
return SESSION_MANAGER_FULL;
}
}
memcpy (curr_session->device_nonce, device_nonce, SESSION_MANAGER_NONCE_LEN);
memcpy (curr_session->cerberus_nonce, cerberus_nonce, SESSION_MANAGER_NONCE_LEN);
memset (curr_session->aes_init_vector, 0, CERBERUS_PROTOCOL_AES_IV_LEN);
curr_session->session_state = SESSION_STATE_SETUP;
curr_session->eid = eid;
curr_session->aes_init_vector[CERBERUS_PROTOCOL_AES_IV_LEN - 1] = 0x80;
return 0;
}
/**
* Generate HMAC of data and compare to received HMAC.
*
* @param session Session manager instance to utilize.
* @param hmac_hash_type HMAC hash type to utilize.
* @param hmac_key HMAC key to utilize.
* @param hmac_key_len HMAC key length.
* @param data Data buffer to compute HMAC of.
* @param data_len Length of data.
* @param hmac HMAC provided by device.
* @param hmac_len HMAC length.
*
* @return Completion status, 0 if success or an error code.
*/
static int session_manager_generate_and_compare_hmac (struct session_manager *session,
enum hmac_hash hmac_hash_type, uint8_t *hmac_key, size_t hmac_key_len, uint8_t *data,
size_t data_len, uint8_t *hmac, size_t hmac_len)
{
uint8_t computed_hmac[SHA256_HASH_LENGTH];
int status;
if (hmac_len != sizeof (computed_hmac)) {
return SESSION_MANAGER_OPERATION_UNSUPPORTED;
}
status = hash_generate_hmac (session->hash, hmac_key, hmac_key_len, data, data_len,
hmac_hash_type, computed_hmac, sizeof (computed_hmac));
if (status != 0) {
return status;
}
if (buffer_compare (computed_hmac, hmac, sizeof (computed_hmac)) != 0) {
return SESSION_MANAGER_OPERATION_NOT_PERMITTED;
}
return 0;
}
/**
* Terminate and reset session.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
* @param hmac Optional HMAC provided by device. Set to NULL if not used.
* @param hmac_len HMAC length.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_reset_session (struct session_manager *session, uint8_t eid, uint8_t *hmac,
size_t hmac_len)
{
struct session_manager_entry *req_session;
int status;
if (session == NULL) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
req_session = session_manager_get_session (session, eid);
if (req_session == NULL) {
return SESSION_MANAGER_UNEXPECTED_EID;
}
if ((hmac != NULL) && (req_session->session_state >= SESSION_STATE_ESTABLISHED)) {
status = session_manager_generate_and_compare_hmac (session, req_session->hmac_hash_type,
req_session->hmac_key, sizeof (req_session->hmac_key), req_session->session_key,
sizeof (req_session->session_key), hmac, hmac_len);
if (status != 0) {
return status;
}
}
memset (req_session, 0, sizeof (struct session_manager_entry));
req_session->session_state = SESSION_STATE_UNUSED;
return 0;
}
/**
* Generate digest of the device and Cerberus session keys. This will leave the hash context open
* for the caller to finish to retrieve the digest value.
*
* @param session Session manager instance to utilize.
* @param device_key Device session public key.
* @param device_key_len Device session public key length.
* @param session_pub_key Cerberus session public key.
* @param session_pub_key_len Cerberus session public key length.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_generate_keys_digest (struct session_manager *session,
const uint8_t *device_key, size_t device_key_len, const uint8_t *session_pub_key,
size_t session_pub_key_len)
{
int status;
if ((session == NULL) || (device_key == NULL) || (session_pub_key == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
status = session->hash->start_sha256 (session->hash);
if (status != 0) {
return status;
}
status = session->hash->update (session->hash, device_key, device_key_len);
if (status != 0) {
session->hash->cancel (session->hash);
return status;
}
status = session->hash->update (session->hash, session_pub_key, session_pub_key_len);
if (status != 0) {
session->hash->cancel (session->hash);
return status;
}
return status;
}
/**
* Setup device binding in an established encrypted session.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
* @param pairing_key_len Length of the pairing key.
* @param pairing_key_hmac Buffer containing HMAC generated by device for pairing key.
* @param pairing_key_hmac_len Length of the pairing key HMAC.
*
* @return Completion status, 0 if success or an error code.
*/
int session_manager_setup_paired_session (struct session_manager *session, uint8_t eid,
size_t pairing_key_len, uint8_t *pairing_key_hmac, size_t pairing_key_hmac_len)
{
struct session_manager_entry *req_session;
uint8_t pairing_key[SHA256_HASH_LENGTH];
uint8_t label[AES_GCM_256_KEY_LENGTH];
uint8_t *pairing_key_buf;
bool pairing_key_generated = false;
char *label_str = "pairing";
int keystore_index;
int status;
if ((session == NULL) || (pairing_key_hmac == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
if (session->store == NULL) {
return SESSION_MANAGER_OPERATION_UNSUPPORTED;
}
req_session = session_manager_get_session (session, eid);
if (req_session == NULL) {
return SESSION_MANAGER_UNEXPECTED_EID;
}
if (req_session->session_state < SESSION_STATE_ESTABLISHED) {
return SESSION_MANAGER_INVALID_ORDER;
}
keystore_index = session_manager_get_paired_key_index (session, eid);
if (ROT_IS_ERROR (keystore_index)) {
return keystore_index;
}
status = session->store->load_key (session->store, keystore_index, &pairing_key_buf,
&pairing_key_len);
if (status == KEYSTORE_NO_KEY) {
status = kdf_nist800_108_counter_mode (session->hash, req_session->hmac_hash_type,
req_session->session_key, sizeof (req_session->session_key), (const uint8_t*) label_str,
strlen (label_str), NULL, 0, pairing_key, sizeof (pairing_key));
if (status != 0) {
return status;
}
pairing_key_generated = true;
}
else if (status != 0) {
return status;
}
else {
memcpy (pairing_key, pairing_key_buf, pairing_key_len);
riot_core_clear (pairing_key_buf, pairing_key_len);
platform_free (pairing_key_buf);
}
status = session_manager_generate_and_compare_hmac (session, req_session->hmac_hash_type,
req_session->hmac_key, sizeof (req_session->hmac_key), pairing_key, sizeof (pairing_key),
pairing_key_hmac, pairing_key_hmac_len);
if (status != 0) {
goto exit;
}
memcpy (label, req_session->session_key, sizeof (label));
status = kdf_nist800_108_counter_mode (session->hash, HMAC_SHA256, pairing_key,
sizeof (pairing_key), label, sizeof (label), NULL, 0, req_session->session_key,
sizeof (req_session->session_key));
if (status != 0) {
goto exit;
}
if (pairing_key_generated) {
status = session->store->save_key (session->store, keystore_index, pairing_key,
sizeof (pairing_key));
if (status != 0) {
goto exit;
}
}
req_session->session_state = SESSION_STATE_PAIRED;
exit:
riot_core_clear (pairing_key, sizeof (pairing_key));
return status;
}
/**
* Get session sync hmac.
*
* @param session Session manager instance to utilize.
* @param eid Device EID.
* @param rn_req Random number provided by device.
* @param hmac Buffer to hold generated HMAC.
* @param hmac_len Size of provided HMAC buffer.
*
* @return Size of generated HMAC or an error code.
*/
int session_manager_session_sync (struct session_manager *session, uint8_t eid, uint32_t rn_req,
uint8_t *hmac, size_t hmac_len)
{
struct session_manager_entry *req_session;
int status;
if ((session == NULL) || (hmac == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
req_session = session_manager_get_session (session, eid);
if (req_session == NULL) {
return SESSION_MANAGER_UNEXPECTED_EID;
}
if (req_session->session_state < SESSION_STATE_ESTABLISHED) {
return SESSION_MANAGER_SESSION_NOT_ESTABLISHED;
}
status = hash_generate_hmac (session->hash, req_session->hmac_key,
sizeof (req_session->hmac_key), (const uint8_t*) &rn_req, sizeof (rn_req),
req_session->hmac_hash_type, hmac, hmac_len);
if (status != 0) {
return status;
}
return SHA256_HASH_LENGTH;
}
/**
* Initialize session manager instance
*
* @param session Session manager instance to initialize.
* @param aes AES engine to utilize for packet encryption/decryption.
* @param hash Hash engine to utilize for AES key generation.
* @param riot RIoT key manager to utilize to get alias key for AES key generation.
* @param sessions_table Preallocated table to use to store session manager entries. Set to NULL to
* dynamically allocate from heap.
* @param num_sessions Number of sessions to support.
* @param pairing_eids List of supported devices for pairing mode. Each element corresponds to a
* device EID, with the element index corresponding to the keystore key ID. The keystore needs to
* be initialized to support storing a key for each device in this array.
* @param num_pairing_eids Total number of supported devices for pairing mode.
* @param store Keystore used to persist pairing keys. Set to NULL if pairing feature not supported.
*
* @return Initialization status, 0 if success or an error code.
*/
int session_manager_init (struct session_manager *session, const struct aes_gcm_engine *aes,
const struct hash_engine *hash, const struct riot_key_manager *riot,
struct session_manager_entry *sessions_table, size_t num_sessions, const uint8_t *pairing_eids,
size_t num_pairing_eids, const struct keystore *store)
{
if ((session == NULL) || (aes == NULL) || (hash == NULL) || (riot == NULL)) {
return SESSION_MANAGER_INVALID_ARGUMENT;
}
memset (session, 0, sizeof (struct session_manager));
session->aes = aes;
session->hash = hash;
session->riot = riot;
session->num_sessions = num_sessions;
session->sessions_table = sessions_table;
session->num_pairing_eids = num_pairing_eids;
session->pairing_eids = pairing_eids;
session->store = store;
if (session->sessions_table == NULL) {
session->sessions_table = (struct session_manager_entry*) platform_calloc (num_sessions,
sizeof (struct session_manager_entry));
if (session->sessions_table == NULL) {
return SESSION_MANAGER_NO_MEMORY;
}
session->sessions_table_preallocated = false;
}
else {
memset (session->sessions_table, 0, sizeof (struct session_manager_entry) * num_sessions);
session->sessions_table_preallocated = true;
}
return 0;
}
/**
* Release session manager
*
* @param session Session manager instance to release
*/
void session_manager_release (struct session_manager *session)
{
if ((session != NULL) && !session->sessions_table_preallocated) {
platform_free (session->sessions_table);
}
}