mirror of
https://github.com/espressif/esp-idf.git
synced 2025-07-29 18:27:20 +02:00
Merge branch 'bugfix/provisioning_sec2_aes_iv_usage_v5.2' into 'release/v5.2'
fix(provisioning): fix incorrect AES-GCM IV usage in security2 scheme (v5.2) See merge request espressif/esp-idf!37616
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -20,6 +20,8 @@
|
||||
#include "esp_local_ctrl.pb-c.h"
|
||||
|
||||
#define ESP_LOCAL_CTRL_VERSION "v1.0"
|
||||
/* JSON format string for version endpoint */
|
||||
#define ESP_LOCAL_CTRL_VER_FMT_STR "{\"local_ctrl\":{\"ver\":\"%s\",\"sec_ver\":%d,\"sec_patch_ver\":%d}}"
|
||||
|
||||
struct inst_ctx {
|
||||
protocomm_t *pc;
|
||||
@ -136,14 +138,6 @@ esp_err_t esp_local_ctrl_start(const esp_local_ctrl_config_t *config)
|
||||
}
|
||||
}
|
||||
|
||||
ret = protocomm_set_version(local_ctrl_inst_ctx->pc, "esp_local_ctrl/version",
|
||||
ESP_LOCAL_CTRL_VERSION);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set version endpoint");
|
||||
esp_local_ctrl_stop();
|
||||
return ret;
|
||||
}
|
||||
|
||||
protocomm_security_t *proto_sec_handle = NULL;
|
||||
switch (local_ctrl_inst_ctx->config.proto_sec.version) {
|
||||
case PROTOCOM_SEC_CUSTOM:
|
||||
@ -183,6 +177,29 @@ esp_err_t esp_local_ctrl_start(const esp_local_ctrl_config_t *config)
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sec_ver = 0;
|
||||
uint8_t sec_patch_ver = 0;
|
||||
protocomm_get_sec_version(local_ctrl_inst_ctx->pc, &sec_ver, &sec_patch_ver);
|
||||
|
||||
const int rsize = snprintf(NULL, 0, ESP_LOCAL_CTRL_VER_FMT_STR, ESP_LOCAL_CTRL_VERSION, sec_ver, sec_patch_ver) + 1;
|
||||
char *ver_str = malloc(rsize);
|
||||
if (!ver_str) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for version string");
|
||||
esp_local_ctrl_stop();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
snprintf(ver_str, rsize, ESP_LOCAL_CTRL_VER_FMT_STR, ESP_LOCAL_CTRL_VERSION, sec_ver, sec_patch_ver);
|
||||
|
||||
ESP_LOGD(TAG, "ver_str: %s", ver_str);
|
||||
ret = protocomm_set_version(local_ctrl_inst_ctx->pc, "esp_local_ctrl/version",
|
||||
ver_str);
|
||||
free(ver_str);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set version endpoint");
|
||||
esp_local_ctrl_stop();
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = protocomm_add_endpoint(local_ctrl_inst_ctx->pc, "esp_local_ctrl/control",
|
||||
esp_local_ctrl_data_handler, NULL);
|
||||
if (ret != ESP_OK) {
|
||||
|
@ -27,6 +27,17 @@ menu "Protocomm"
|
||||
Consult the Enabling protocomm security version section of the
|
||||
Protocomm documentation in ESP-IDF Programming guide for more details.
|
||||
|
||||
config ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION
|
||||
bool
|
||||
default y
|
||||
help
|
||||
Enable support of security patch version. This is a hidden config option
|
||||
kept for external components like "network_provisioning" to find out if
|
||||
protocomm component support security patch version. This config option
|
||||
also indicates availability of a new API `protocomm_get_sec_version`.
|
||||
Please refer to Protocomm documentation in ESP-IDF Programming guide for
|
||||
more details.
|
||||
|
||||
config ESP_PROTOCOMM_KEEP_BLE_ON_AFTER_BLE_STOP
|
||||
bool
|
||||
depends on BT_ENABLED
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -260,6 +260,21 @@ esp_err_t protocomm_set_version(protocomm_t *pc, const char *ep_name,
|
||||
*/
|
||||
esp_err_t protocomm_unset_version(protocomm_t *pc, const char *ep_name);
|
||||
|
||||
/**
|
||||
* @brief Get the security version of the protocomm instance
|
||||
*
|
||||
* This API will return the security version of the protocomm instance.
|
||||
*
|
||||
* @param[in] pc Pointer to the protocomm instance
|
||||
* @param[out] sec_ver Pointer to the security version
|
||||
* @param[out] sec_patch_ver Pointer to the security patch version
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK : Success
|
||||
* - ESP_ERR_INVALID_ARG : Null instance/name arguments
|
||||
*/
|
||||
esp_err_t protocomm_get_sec_version(protocomm_t *pc, int *sec_ver, uint8_t *sec_patch_ver);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -86,6 +86,11 @@ typedef struct protocomm_security {
|
||||
*/
|
||||
int ver;
|
||||
|
||||
/**
|
||||
* Patch version number of security implementation
|
||||
*/
|
||||
uint8_t patch_ver;
|
||||
|
||||
/**
|
||||
* Function for initializing/allocating security
|
||||
* infrastructure
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -426,3 +426,15 @@ esp_err_t protocomm_unset_version(protocomm_t *pc, const char *ep_name)
|
||||
|
||||
return protocomm_remove_endpoint(pc, ep_name);
|
||||
}
|
||||
|
||||
esp_err_t protocomm_get_sec_version(protocomm_t *pc, int *sec_ver, uint8_t *sec_patch_ver)
|
||||
{
|
||||
if (pc == NULL || sec_ver == NULL || sec_patch_ver == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*sec_ver = pc->sec->ver;
|
||||
*sec_patch_ver = pc->sec->patch_ver;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -14,7 +14,8 @@
|
||||
|
||||
#include <mbedtls/gcm.h>
|
||||
#include <mbedtls/error.h>
|
||||
#include <esp_random.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
|
||||
#include <protocomm_security.h>
|
||||
#include <protocomm_security2.h>
|
||||
@ -24,6 +25,7 @@
|
||||
#include "constants.pb-c.h"
|
||||
|
||||
#include "esp_srp.h"
|
||||
#include "endian.h"
|
||||
|
||||
static const char *TAG = "security2";
|
||||
|
||||
@ -33,13 +35,21 @@ ESP_EVENT_DEFINE_BASE(PROTOCOMM_SECURITY_SESSION_EVENT);
|
||||
#define PUBLIC_KEY_LEN (384)
|
||||
#define CLIENT_PROOF_LEN (64)
|
||||
#define AES_GCM_KEY_LEN (256)
|
||||
#define AES_GCM_IV_SIZE (16)
|
||||
#define AES_GCM_IV_SIZE (12)
|
||||
#define AES_GCM_TAG_LEN (16)
|
||||
#define SESSION_ID_LEN (8)
|
||||
|
||||
#define SESSION_STATE_CMD0 0 /* Session is not setup: Initial State*/
|
||||
#define SESSION_STATE_CMD1 1 /* Session is not setup: Cmd0 done */
|
||||
#define SESSION_STATE_DONE 2 /* Session setup successful */
|
||||
|
||||
typedef struct aes_gcm_iv {
|
||||
uint8_t session_id[SESSION_ID_LEN];
|
||||
uint32_t counter;
|
||||
} aes_gcm_iv_t;
|
||||
|
||||
static_assert(sizeof(aes_gcm_iv_t) == AES_GCM_IV_SIZE, "Invalid size of AES GCM IV");
|
||||
|
||||
typedef struct session {
|
||||
/* Session data */
|
||||
uint32_t id;
|
||||
@ -65,6 +75,12 @@ static void hexdump(const char *msg, char *buf, int len)
|
||||
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buf, len, ESP_LOG_DEBUG);
|
||||
}
|
||||
|
||||
static inline void sec2_gcm_iv_counter_increment(uint8_t *iv_buf)
|
||||
{
|
||||
aes_gcm_iv_t *iv = (aes_gcm_iv_t *) iv_buf;
|
||||
iv->counter = htobe32(be32toh(iv->counter) + 1);
|
||||
}
|
||||
|
||||
static esp_err_t sec2_new_session(protocomm_security_handle_t handle, uint32_t session_id);
|
||||
|
||||
static esp_err_t handle_session_command0(session_t *cur_session,
|
||||
@ -172,19 +188,21 @@ static esp_err_t handle_session_command0(session_t *cur_session,
|
||||
out->payload_case = SEC2_PAYLOAD__PAYLOAD_SR0;
|
||||
out->sr0 = out_resp;
|
||||
|
||||
resp->sec_ver = SEC_SCHEME_VERSION__SecScheme2;
|
||||
resp->proto_case = SESSION_DATA__PROTO_SEC2;
|
||||
resp->sec2 = out;
|
||||
|
||||
cur_session->username_len = in->sc0->client_username.len;
|
||||
cur_session->username = malloc(cur_session->username_len);
|
||||
if (!cur_session->username) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory!");
|
||||
esp_srp_free(cur_session->srp_hd);
|
||||
free(out);
|
||||
free(out_resp);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
memcpy(cur_session->username, in->sc0->client_username.data, in->sc0->client_username.len);
|
||||
|
||||
resp->sec_ver = SEC_SCHEME_VERSION__SecScheme2;
|
||||
resp->proto_case = SESSION_DATA__PROTO_SEC2;
|
||||
resp->sec2 = out;
|
||||
|
||||
cur_session->state = SESSION_STATE_CMD1;
|
||||
|
||||
ESP_LOGD(TAG, "Session setup phase1 done");
|
||||
@ -224,15 +242,35 @@ static esp_err_t handle_session_command1(session_t *cur_session,
|
||||
}
|
||||
hexdump("Device proof", device_proof, CLIENT_PROOF_LEN);
|
||||
|
||||
/* Initialize crypto context */
|
||||
mbedtls_gcm_init(&cur_session->ctx_gcm);
|
||||
mbedtls_entropy_context entropy;
|
||||
mbedtls_ctr_drbg_context ctr_drbg;
|
||||
|
||||
/* Considering the protocomm component is only used after RF ( Wifi/Bluetooth ) is enabled.
|
||||
* Hence, we can be sure that the RNG generates true random numbers */
|
||||
esp_fill_random(&cur_session->iv, AES_GCM_IV_SIZE);
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
|
||||
int ret;
|
||||
ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Failed to seed random number generator");
|
||||
free(device_proof);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
aes_gcm_iv_t *iv = (aes_gcm_iv_t *) cur_session->iv;
|
||||
ret = mbedtls_ctr_drbg_random(&ctr_drbg, iv->session_id, SESSION_ID_LEN);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Failed to generate random number");
|
||||
free(device_proof);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* Initialize counter value to 1 */
|
||||
iv->counter = htobe32(0x1);
|
||||
|
||||
hexdump("Initialization vector", (char *)cur_session->iv, AES_GCM_IV_SIZE);
|
||||
|
||||
/* Initialize crypto context */
|
||||
mbedtls_gcm_init(&cur_session->ctx_gcm);
|
||||
|
||||
mbed_err = mbedtls_gcm_setkey(&cur_session->ctx_gcm, MBEDTLS_CIPHER_ID_AES, (unsigned char *)cur_session->session_key, AES_GCM_KEY_LEN);
|
||||
if (mbed_err != 0) {
|
||||
ESP_LOGE(TAG, "Failure at mbedtls_gcm_setkey_enc with error code : -0x%x", -mbed_err);
|
||||
@ -429,6 +467,13 @@ static esp_err_t sec2_encrypt(protocomm_security_handle_t handle,
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
aes_gcm_iv_t *iv = (aes_gcm_iv_t *) cur_session->iv;
|
||||
if (be32toh(iv->counter) == 0) {
|
||||
ESP_LOGE(TAG, "Invalid counter value, restart session");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
hexdump("Encrypt IV", (char *)cur_session->iv, AES_GCM_IV_SIZE);
|
||||
|
||||
*outlen = inlen + AES_GCM_TAG_LEN;
|
||||
*outbuf = (uint8_t *) malloc(*outlen);
|
||||
if (!*outbuf) {
|
||||
@ -446,6 +491,9 @@ static esp_err_t sec2_encrypt(protocomm_security_handle_t handle,
|
||||
}
|
||||
memcpy(*outbuf + inlen, gcm_tag, AES_GCM_TAG_LEN);
|
||||
|
||||
/* Increment counter value for next operation */
|
||||
sec2_gcm_iv_counter_increment(cur_session->iv);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@ -469,6 +517,13 @@ static esp_err_t sec2_decrypt(protocomm_security_handle_t handle,
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
aes_gcm_iv_t *iv = (aes_gcm_iv_t *) cur_session->iv;
|
||||
if (be32toh(iv->counter) == 0) {
|
||||
ESP_LOGE(TAG, "Invalid counter value, restart session");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
hexdump("Decrypt IV", (char *)cur_session->iv, AES_GCM_IV_SIZE);
|
||||
|
||||
*outlen = inlen - AES_GCM_TAG_LEN;
|
||||
*outbuf = (uint8_t *) malloc(*outlen);
|
||||
if (!*outbuf) {
|
||||
@ -482,6 +537,10 @@ static esp_err_t sec2_decrypt(protocomm_security_handle_t handle,
|
||||
ESP_LOGE(TAG, "Failed at mbedtls_gcm_auth_decrypt : %d", ret);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Increment counter value for next operation */
|
||||
sec2_gcm_iv_counter_increment(cur_session->iv);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@ -543,6 +602,7 @@ static esp_err_t sec2_req_handler(protocomm_security_handle_t handle,
|
||||
|
||||
const protocomm_security_t protocomm_security2 = {
|
||||
.ver = 2,
|
||||
.patch_ver = 1,
|
||||
.init = sec2_init,
|
||||
.cleanup = sec2_cleanup,
|
||||
.new_transport_session = sec2_new_session,
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@ -156,7 +156,7 @@ static struct wifi_prov_mgr_ctx *prov_ctx;
|
||||
|
||||
/* This executes registered app_event_callback for a particular event
|
||||
*
|
||||
* NOTE : By the time this fucntion returns, it is possible that
|
||||
* NOTE : By the time this function returns, it is possible that
|
||||
* the manager got de-initialized due to a call to wifi_prov_mgr_deinit()
|
||||
* either inside the event callbacks or from another thread. Therefore
|
||||
* post execution of execute_event_cb(), the validity of prov_ctx must
|
||||
@ -257,7 +257,15 @@ static cJSON* wifi_prov_get_info_json(void)
|
||||
/* Version field */
|
||||
cJSON_AddStringToObject(prov_info_json, "ver", prov_ctx->mgr_info.version);
|
||||
|
||||
/* Security field */
|
||||
int sec_ver = 0;
|
||||
uint8_t sec_patch_ver = 0;
|
||||
|
||||
protocomm_get_sec_version(prov_ctx->pc, &sec_ver, &sec_patch_ver);
|
||||
assert(sec_ver == prov_ctx->security);
|
||||
cJSON_AddNumberToObject(prov_info_json, "sec_ver", prov_ctx->security);
|
||||
cJSON_AddNumberToObject(prov_info_json, "sec_patch_ver", sec_patch_ver);
|
||||
|
||||
/* Capabilities field */
|
||||
cJSON_AddItemToObject(prov_info_json, "cap", prov_capabilities);
|
||||
|
||||
@ -304,19 +312,6 @@ static esp_err_t wifi_prov_mgr_start_service(const char *service_name, const cha
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Set version information / capabilities of provisioning service and application */
|
||||
cJSON *version_json = wifi_prov_get_info_json();
|
||||
char *version_str = cJSON_Print(version_json);
|
||||
ret = protocomm_set_version(prov_ctx->pc, "proto-ver", version_str);
|
||||
free(version_str);
|
||||
cJSON_Delete(version_json);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set version endpoint");
|
||||
scheme->prov_stop(prov_ctx->pc);
|
||||
protocomm_delete(prov_ctx->pc);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Set protocomm security type for endpoint */
|
||||
if (prov_ctx->security == 0) {
|
||||
#ifdef CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0
|
||||
@ -353,6 +348,21 @@ static esp_err_t wifi_prov_mgr_start_service(const char *service_name, const cha
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Set version information / capabilities of provisioning service and application */
|
||||
cJSON *version_json = wifi_prov_get_info_json();
|
||||
char *version_str = cJSON_Print(version_json);
|
||||
ESP_LOGD(TAG, "version_str :%s:", version_str);
|
||||
|
||||
ret = protocomm_set_version(prov_ctx->pc, "proto-ver", version_str);
|
||||
free(version_str);
|
||||
cJSON_Delete(version_json);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set version endpoint");
|
||||
scheme->prov_stop(prov_ctx->pc);
|
||||
protocomm_delete(prov_ctx->pc);
|
||||
return ret;
|
||||
}
|
||||
|
||||
prov_ctx->wifi_prov_handlers = malloc(sizeof(wifi_prov_config_handlers_t));
|
||||
ret = get_wifi_prov_handlers(prov_ctx->wifi_prov_handlers);
|
||||
if (ret != ESP_OK) {
|
||||
@ -1594,7 +1604,6 @@ esp_err_t wifi_prov_mgr_start_provisioning(wifi_prov_security_t security, const
|
||||
#endif
|
||||
prov_ctx->security = security;
|
||||
|
||||
|
||||
esp_timer_create_args_t wifi_connect_timer_conf = {
|
||||
.callback = wifi_connect_timer_cb,
|
||||
.arg = NULL,
|
||||
|
@ -251,14 +251,56 @@ Details about the Security 2 scheme are shown in the below sequence diagram:
|
||||
device verifies this M1 with the M1 obtained from Client"];
|
||||
DEVICE -> DEVICE [label = "Verification\nToken", leftnote = "
|
||||
Device generate device_proof M2 = H(A, M, K)"];
|
||||
DEVICE -> DEVICE [label = "Initialization\nVector", leftnote = "dev_rand = gen_16byte_random()
|
||||
This random number is to be used for AES-GCM operation
|
||||
for encryption and decryption of the data using the shared secret"];
|
||||
DEVICE -> DEVICE [label = "Initialization\nVector", leftnote = "dev_rand = gen_12byte_iv()
|
||||
This random number is formed as session_id (8byte) + counter (4byte)
|
||||
to be used for AES-GCM operation for encryption and decryption of
|
||||
the data using the shared secret"];
|
||||
DEVICE -> CLIENT [label = "SessionResp1(device_proof M2, dev_rand)"];
|
||||
CLIENT -> CLIENT [label = "Verify Device", rightnote = "Client calculates device proof M2 as M2 = H(A, M, K)
|
||||
client verifies this M2 with M2 obtained from device"];
|
||||
}
|
||||
|
||||
|
||||
Security 2 AES-GCM IV Handling
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Security 2 scheme uses AES-GCM for encryption and decryption of the data. The initialization vector (IV) consists of an 8-byte session ID and a 4-byte counter, for a total of 12 bytes. The counter starts at 1 and is incremented after each encryption/decryption operation on both the device and the client.
|
||||
|
||||
.. seqdiag::
|
||||
:caption: Security 2 AES-GCM IV Handling
|
||||
:align: center
|
||||
|
||||
seqdiag security2_gcm {
|
||||
activation = none;
|
||||
node_width = 80;
|
||||
node_height = 60;
|
||||
edge_length = 550;
|
||||
span_height = 5;
|
||||
default_shape = roundedbox;
|
||||
default_fontsize = 12;
|
||||
|
||||
CLIENT [label = "Client\n(PhoneApp)"];
|
||||
DEVICE [label = "Device\n(ESP)"];
|
||||
|
||||
=== Security 2 AES-GCM IV Handling ===
|
||||
DEVICE -> DEVICE [label = "Initialize\nIV", leftnote = "Initial IV = session_id (8 bytes) || counter (4 bytes)
|
||||
session_id = random 8 byte value
|
||||
counter = 0x1 (stored as big-endian)"];
|
||||
DEVICE -> CLIENT [label = "Send 12-byte IV to client (session_id || counter)"];
|
||||
CLIENT -> CLIENT [label = "Initialize\nIV", rightnote = "Set initial IV from device:
|
||||
- session_id (8 bytes from device)
|
||||
- counter = 0x1"];
|
||||
CLIENT -> DEVICE [label = "First Encrypted Command using initial IV"];
|
||||
CLIENT -> CLIENT [label = "Increment\nCounter", rightnote = "After first command:
|
||||
- Increment counter to 0x2
|
||||
- New IV = session_id || counter"];
|
||||
DEVICE -> DEVICE [label = "Increment\nCounter", leftnote = "Before first response:
|
||||
- Increment counter to 0x2
|
||||
- New IV = session_id || counter"];
|
||||
DEVICE -> CLIENT [label = "Encrypted Response using updated IV"];
|
||||
}
|
||||
|
||||
|
||||
Sample Code
|
||||
>>>>>>>>>>>
|
||||
|
||||
|
@ -217,7 +217,19 @@ Once connected to the device, the provisioning-related protocomm endpoints can b
|
||||
- http://<mdns-hostname>.local/proto-ver
|
||||
- the endpoint for retrieving version info
|
||||
|
||||
Immediately after connecting, the client application may fetch the version/capabilities information from the ``proto-ver`` endpoint. All communications to this endpoint are unencrypted, hence necessary information, which may be relevant for deciding compatibility, can be retrieved before establishing a secure session. The response is in JSON format and looks like : ``prov: { ver: v1.1, cap: [no_pop] }, my_app: { ver: 1.345, cap: [cloud, local_ctrl] },....``. Here label ``prov`` provides provisioning service version ``ver`` and capabilities ``cap``. For now, only the ``no_pop`` capability is supported, which indicates that the service does not require proof of possession for authentication. Any application-related version or capabilities are given by other labels, e.g., ``my_app`` in this example. These additional fields are set using :cpp:func:`wifi_prov_mgr_set_app_info()`.
|
||||
Immediately after connecting, the client application may fetch the version/capabilities information from the ``proto-ver`` endpoint. All communications to this endpoint are unencrypted, hence necessary information, which may be relevant for deciding compatibility, can be retrieved before establishing a secure session. The response is in JSON format and looks like : ``prov: { ver: v1.1, sec_ver: 1, sec_patch_ver: 0, cap: [no_pop] }, my_app: { ver: 1.345, cap: [cloud, local_ctrl] },....``.
|
||||
|
||||
Here label ``prov`` provides:
|
||||
|
||||
- provisioning service version ``ver``
|
||||
- security version ``sec_ver``
|
||||
- security patch version ``sec_patch_ver`` (default is 0)
|
||||
- capabilities ``cap``
|
||||
|
||||
For now, only the ``no_pop`` capability is supported, which indicates that the service does not require proof of possession for authentication. Any application-related version or capabilities are given by other labels, e.g., ``my_app`` in this example. These additional fields are set using :cpp:func:`wifi_prov_mgr_set_app_info()`.
|
||||
|
||||
.. important::
|
||||
Client must take into account both the ``sec_ver`` and ``sec_patch_ver`` fields, as these are used to determine the security scheme to be used for the session establishment.
|
||||
|
||||
User side applications need to implement the signature handshaking required for establishing and authenticating secure protocomm sessions as per the security scheme configured for use, which is not needed when the manager is configured to use protocomm security 0.
|
||||
|
||||
|
@ -251,14 +251,56 @@ Security 2 方案基于 Secure Remote Password (SRP6a) 协议,详情请参阅
|
||||
设备将该 M1 值与从客户端获得的 M1 进行验证"];
|
||||
DEVICE -> DEVICE [label = "验证令牌", leftnote = "
|
||||
设备生成 device_proof M2 = H(A, M, K)"];
|
||||
DEVICE -> DEVICE [label = "初始化向量", leftnote = "dev_rand = gen_16byte_random()
|
||||
该随机数通常用作 AES-GCM 操作,
|
||||
并使用共享密钥加密和解密数据"];
|
||||
DEVICE -> DEVICE [label = "初始化向量", leftnote = "dev_rand = gen_12byte_iv()
|
||||
该随机数由 session_id(8 字节)和 counter(4 字节)组成,
|
||||
用于 AES-GCM 操作,并使用共享密钥对数据进行加密和解密"];
|
||||
DEVICE -> CLIENT [label = "SessionResp1(device_proof M2, dev_rand)"];
|
||||
CLIENT -> CLIENT [label = "验证设备", rightnote = "客户端计算设备证明 M2 = H(A, M, K),
|
||||
客户端将该 M2 值与从设备获得的 M2 进行验证"];
|
||||
}
|
||||
|
||||
|
||||
|
||||
Security 2 AES-GCM IV 处理
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Security 2 方案使用 AES-GCM 对数据进行加密和解密。初始化向量 (IV) 由 8 字节的会话 ID (session_id) 和 4 字节的计数器 (counter) 组成,总计 12 字节。counter 从 1 开始,并在设备和客户端每次执行加密/解密操作后递增。
|
||||
|
||||
.. seqdiag::
|
||||
:caption: Security 2 AES-GCM IV 处理
|
||||
:align: center
|
||||
|
||||
seqdiag security2_gcm {
|
||||
activation = none;
|
||||
node_width = 80;
|
||||
node_height = 60;
|
||||
edge_length = 550;
|
||||
span_height = 5;
|
||||
default_shape = roundedbox;
|
||||
default_fontsize = 12;
|
||||
|
||||
CLIENT [label = "客户端\n(手机应用)"];
|
||||
DEVICE [label = "设备\n(ESP)"];
|
||||
|
||||
=== Security 2 AES-GCM IV 处理 ===
|
||||
DEVICE -> DEVICE [label = "初始化 IV", leftnote = "初始 IV = session_id (8 字节) || counter (4 字节)
|
||||
session_id = 随机 8 字节值
|
||||
counter = 0x1(以大端模式存储)"];
|
||||
DEVICE -> CLIENT [label = "将 12 字节的 IV 发送给客户端 (session_id || counter)"];
|
||||
CLIENT -> CLIENT [label = "初始化 IV", rightnote = "从设备获取并设置初始 IV:
|
||||
- session_id(来自设备的 8 字节)
|
||||
- counter = 0x1"];
|
||||
CLIENT -> DEVICE [label = "使用初始 IV 发送第一个加密指令"];
|
||||
CLIENT -> CLIENT [label = "递增 counter", rightnote = "在第一个指令后:
|
||||
- counter 递增至 0x2
|
||||
- 新 IV = session_id || counter"];
|
||||
DEVICE -> DEVICE [label = "递增 counter", leftnote = "在第一个响应前:
|
||||
- counter 递增至 0x2
|
||||
- 新 IV = session_id || counter"];
|
||||
DEVICE -> CLIENT [label = "使用更新后的 IV 发送加密响应"];
|
||||
}
|
||||
|
||||
|
||||
示例代码
|
||||
>>>>>>>>>>>
|
||||
|
||||
|
@ -217,7 +217,19 @@ Wi-Fi 配网
|
||||
- http://<mdns-hostname>.local/proto-ver
|
||||
- 用于获取版本信息的端点
|
||||
|
||||
连接后,客户端应用程序可以立即从 ``proto-ver`` 端点获取版本或属性信息。所有与此端点的通信均未加密,因此在建立安全会话前,可以检索相关必要信息,确保会话兼容。响应结果以 JSON 格式返回,格式类似于 ``prov: { ver: v1.1, cap: [no_pop] }, my_app: { ver: 1.345, cap: [cloud, local_ctrl] },....``。其中 ``prov`` 标签提供了配网服务的版本 ``ver`` 和属性 ``cap``。目前仅支持 ``no_pop`` 属性,表示该服务不需要验证所有权证明。任何与应用程序相关的版本或属性将由其他标签给出,如本示例中的 ``my_app``。使用 :cpp:func:`wifi_prov_mgr_set_app_info()` 可以设置这些附加字段。
|
||||
连接后,客户端应用程序可以立即从 ``proto-ver`` 端点获取版本或功能信息。所有与此端点的通信均未加密,因此在建立安全会话之前,可以检索相关必要信息,确保会话兼容。响应数据采用 JSON 格式,示例如下:``prov: { ver: v1.1, sec_ver: 1, sec_patch_ver: 0, cap: [no_pop] }, my_app: { ver: 1.345, cap: [cloud, local_ctrl] },....``。
|
||||
|
||||
其中,``prov`` 标签提供以下信息:
|
||||
|
||||
- 配网服务的版本 ``ver``
|
||||
- 安全版本 ``sec_ver``
|
||||
- 安全补丁版本 ``sec_patch_ver`` (默认为 0)
|
||||
- 功能 ``cap``
|
||||
|
||||
目前仅支持 ``no_pop`` 功能,该功能表示服务无需用户提供所有权证明即可进行身份验证。任何与应用程序相关的版本或功能将由其他标签提供,如上述示例中的 ``my_app``。使用 :cpp:func:`wifi_prov_mgr_set_app_info()` 可以设置这些附加字段。
|
||||
|
||||
.. important::
|
||||
建立会话时,客户端应依据 ``sec_ver`` 和 ``sec_patch_ver`` 字段来确定使用何种安全方案。
|
||||
|
||||
用户端应用程序需要根据所配置的安全方案实现签名握手,以建立和认证 protocomm 安全会话。当管理器配置为使用 protocomm security 0 时,则不需要实现签名握手。
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
@ -113,9 +112,9 @@ def on_except(err):
|
||||
print(err)
|
||||
|
||||
|
||||
def get_security(secver, username, password, pop='', verbose=False):
|
||||
def get_security(secver, sec_patch_ver, username, password, pop='', verbose=False):
|
||||
if secver == 2:
|
||||
return security.Security2(username, password, verbose)
|
||||
return security.Security2(sec_patch_ver, username, password, verbose)
|
||||
if secver == 1:
|
||||
return security.Security1(pop, verbose)
|
||||
if secver == 0:
|
||||
@ -148,6 +147,32 @@ async def get_transport(sel_transport, service_name, check_hostname):
|
||||
return None
|
||||
|
||||
|
||||
async def get_sec_patch_ver(tp, verbose=False):
|
||||
try:
|
||||
response = await tp.send_data('esp_local_ctrl/version', '---')
|
||||
|
||||
if verbose:
|
||||
print('esp_local_ctrl/version response : ', response)
|
||||
|
||||
try:
|
||||
# Interpret this as JSON structure containing
|
||||
# information with security version information
|
||||
info = json.loads(response)
|
||||
try:
|
||||
sec_patch_ver = info['local_ctrl']['sec_patch_ver']
|
||||
except KeyError:
|
||||
sec_patch_ver = 0
|
||||
return sec_patch_ver
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, we assume default patch level
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
on_except(e)
|
||||
return None
|
||||
|
||||
|
||||
async def version_match(tp, protover, verbose=False):
|
||||
try:
|
||||
response = await tp.send_data('esp_local_ctrl/version', protover)
|
||||
@ -164,7 +189,7 @@ async def version_match(tp, protover, verbose=False):
|
||||
# information with versions and capabilities of both
|
||||
# provisioning service and application
|
||||
info = json.loads(response)
|
||||
if info['prov']['ver'].lower() == protover.lower():
|
||||
if info['local_ctrl']['ver'].lower() == protover.lower():
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
@ -191,14 +216,19 @@ async def has_capability(tp, capability='none', verbose=False):
|
||||
# information with versions and capabilities of both
|
||||
# provisioning service and application
|
||||
info = json.loads(response)
|
||||
supported_capabilities = info['prov']['cap']
|
||||
if capability.lower() == 'none':
|
||||
# No specific capability to check, but capabilities
|
||||
# feature is present so return True
|
||||
return True
|
||||
elif capability in supported_capabilities:
|
||||
return True
|
||||
return False
|
||||
try:
|
||||
supported_capabilities = info['local_ctrl']['cap']
|
||||
if capability.lower() == 'none':
|
||||
# No specific capability to check, but capabilities
|
||||
# feature is present so return True
|
||||
return True
|
||||
elif capability in supported_capabilities:
|
||||
return True
|
||||
return False
|
||||
except KeyError:
|
||||
# If capabilities field is not present, it means
|
||||
# that capabilities are not supported
|
||||
return False
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, it means that capabilities
|
||||
@ -341,6 +371,7 @@ async def main():
|
||||
if obj_transport is None:
|
||||
raise RuntimeError('Failed to establish connection')
|
||||
|
||||
sec_patch_ver = 0
|
||||
# If security version not specified check in capabilities
|
||||
if args.secver is None:
|
||||
# First check if capabilities are supported or not
|
||||
@ -362,13 +393,14 @@ async def main():
|
||||
args.pop = ''
|
||||
|
||||
if (args.secver == 2):
|
||||
sec_patch_ver = await get_sec_patch_ver(obj_transport, args.verbose)
|
||||
if len(args.sec2_usr) == 0:
|
||||
args.sec2_usr = input('Security Scheme 2 - SRP6a Username required: ')
|
||||
if len(args.sec2_pwd) == 0:
|
||||
prompt_str = 'Security Scheme 2 - SRP6a Password required: '
|
||||
args.sec2_pwd = getpass(prompt_str)
|
||||
|
||||
obj_security = get_security(args.secver, args.sec2_usr, args.sec2_pwd, args.pop, args.verbose)
|
||||
obj_security = get_security(args.secver, sec_patch_ver, args.sec2_usr, args.sec2_pwd, args.pop, args.verbose)
|
||||
if obj_security is None:
|
||||
raise ValueError('Invalid Security Version')
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
@ -38,9 +37,9 @@ def on_except(err):
|
||||
print(err)
|
||||
|
||||
|
||||
def get_security(secver, username, password, pop='', verbose=False):
|
||||
def get_security(secver, sec_patch_ver, username, password, pop='', verbose=False):
|
||||
if secver == 2:
|
||||
return security.Security2(username, password, verbose)
|
||||
return security.Security2(sec_patch_ver, username, password, verbose)
|
||||
elif secver == 1:
|
||||
return security.Security1(pop, verbose)
|
||||
elif secver == 0:
|
||||
@ -148,6 +147,27 @@ async def get_version(tp):
|
||||
return response
|
||||
|
||||
|
||||
async def get_sec_patch_ver(tp, verbose=False):
|
||||
response = await get_version(tp)
|
||||
|
||||
if verbose:
|
||||
print('proto-ver response : ', response)
|
||||
|
||||
try:
|
||||
# Interpret this as JSON structure containing
|
||||
# information with security version information
|
||||
info = json.loads(response)
|
||||
try:
|
||||
sec_patch_ver = info['prov']['sec_patch_ver']
|
||||
except KeyError:
|
||||
sec_patch_ver = 0
|
||||
return sec_patch_ver
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, we assume default patch level
|
||||
return 0
|
||||
|
||||
|
||||
async def establish_session(tp, sec):
|
||||
try:
|
||||
response = None
|
||||
@ -415,6 +435,7 @@ async def main():
|
||||
raise RuntimeError('Failed to establish connection')
|
||||
|
||||
try:
|
||||
sec_patch_ver = 0
|
||||
# If security version not specified check in capabilities
|
||||
if args.secver is None:
|
||||
# First check if capabilities are supported or not
|
||||
@ -436,13 +457,14 @@ async def main():
|
||||
args.sec1_pop = ''
|
||||
|
||||
if (args.secver == 2):
|
||||
sec_patch_ver = await get_sec_patch_ver(obj_transport, args.verbose)
|
||||
if len(args.sec2_usr) == 0:
|
||||
args.sec2_usr = input('Security Scheme 2 - SRP6a Username required: ')
|
||||
if len(args.sec2_pwd) == 0:
|
||||
prompt_str = 'Security Scheme 2 - SRP6a Password required: '
|
||||
args.sec2_pwd = getpass(prompt_str)
|
||||
|
||||
obj_security = get_security(args.secver, args.sec2_usr, args.sec2_pwd, args.sec1_pop, args.verbose)
|
||||
obj_security = get_security(args.secver, sec_patch_ver, args.sec2_usr, args.sec2_pwd, args.sec1_pop, args.verbose)
|
||||
if obj_security is None:
|
||||
raise ValueError('Invalid Security Version')
|
||||
|
||||
@ -459,7 +481,7 @@ async def main():
|
||||
print('==== Session Established ====')
|
||||
|
||||
if args.reset:
|
||||
print('==== Reseting WiFi====')
|
||||
print('==== Resetting WiFi====')
|
||||
await reset_wifi(obj_transport, obj_security)
|
||||
sys.exit()
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# APIs for interpreting and creating protobuf packets for
|
||||
# protocomm endpoint with security type protocomm_security2
|
||||
|
||||
|
||||
from typing import Any, Type
|
||||
import struct
|
||||
from typing import Any
|
||||
from typing import Type
|
||||
|
||||
import proto
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from utils import long_to_bytes, str_to_bytes
|
||||
from utils import long_to_bytes
|
||||
from utils import str_to_bytes
|
||||
|
||||
from .security import Security
|
||||
from .srp6a import Srp6a, generate_salt_and_verifier
|
||||
from .srp6a import generate_salt_and_verifier
|
||||
from .srp6a import Srp6a
|
||||
|
||||
AES_KEY_LEN = 256 // 8
|
||||
|
||||
@ -30,17 +31,18 @@ def sec2_gen_salt_verifier(username: str, password: str, salt_len: int) -> Any:
|
||||
|
||||
salt_str = ', '.join([format(b, '#04x') for b in salt])
|
||||
salt_c_arr = '\n '.join(salt_str[i: i + 96] for i in range(0, len(salt_str), 96))
|
||||
print(f'static const char sec2_salt[] = {{\n {salt_c_arr}\n}};\n')
|
||||
print(f'static const char sec2_salt[] = {{\n {salt_c_arr}\n}};\n') # noqa E702
|
||||
|
||||
verifier_str = ', '.join([format(b, '#04x') for b in verifier])
|
||||
verifier_c_arr = '\n '.join(verifier_str[i: i + 96] for i in range(0, len(verifier_str), 96))
|
||||
print(f'static const char sec2_verifier[] = {{\n {verifier_c_arr}\n}};\n')
|
||||
print(f'static const char sec2_verifier[] = {{\n {verifier_c_arr}\n}};\n') # noqa E702
|
||||
|
||||
|
||||
class Security2(Security):
|
||||
def __init__(self, username: str, password: str, verbose: bool) -> None:
|
||||
def __init__(self, sec_patch_ver:int, username: str, password: str, verbose: bool) -> None:
|
||||
# Initialize state of the security2 FSM
|
||||
self.session_state = security_state.REQUEST1
|
||||
self.sec_patch_ver = sec_patch_ver
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.verbose = verbose
|
||||
@ -49,7 +51,7 @@ class Security2(Security):
|
||||
self.cipher: Type[AESGCM]
|
||||
|
||||
self.client_pop_key = None
|
||||
self.nonce = None
|
||||
self.nonce = bytearray()
|
||||
|
||||
Security.__init__(self, self.security2_session)
|
||||
|
||||
@ -75,7 +77,7 @@ class Security2(Security):
|
||||
|
||||
def _print_verbose(self, data: str) -> None:
|
||||
if (self.verbose):
|
||||
print(f'\x1b[32;20m++++ {data} ++++\x1b[0m')
|
||||
print(f'\x1b[32;20m++++ {data} ++++\x1b[0m') # noqa E702
|
||||
|
||||
def setup0_request(self) -> Any:
|
||||
# Form SessionCmd0 request packet using client public key
|
||||
@ -148,7 +150,7 @@ class Security2(Security):
|
||||
self._print_verbose(f'Session Key:\t0x{session_key.hex()}')
|
||||
|
||||
# 96-bit nonce
|
||||
self.nonce = setup_resp.sec2.sr1.device_nonce
|
||||
self.nonce = bytearray(setup_resp.sec2.sr1.device_nonce)
|
||||
if self.nonce is None:
|
||||
raise RuntimeError('Received invalid nonce from device!')
|
||||
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
|
||||
@ -158,8 +160,23 @@ class Security2(Security):
|
||||
if self.cipher is None:
|
||||
raise RuntimeError('Failed to initialize AES-GCM cryptographic engine!')
|
||||
|
||||
def _increment_nonce(self) -> None:
|
||||
"""Increment the last 4 bytes of nonce (big-endian counter)."""
|
||||
if self.sec_patch_ver == 1:
|
||||
counter = struct.unpack('>I', self.nonce[8:])[0] # Read last 4 bytes as big-endian integer
|
||||
counter += 1 # Increment counter
|
||||
if counter > 0xFFFFFFFF: # Check for overflow
|
||||
raise RuntimeError('Nonce counter overflow')
|
||||
self.nonce[8:] = struct.pack('>I', counter) # Store back as big-endian
|
||||
|
||||
def encrypt_data(self, data: bytes) -> Any:
|
||||
return self.cipher.encrypt(self.nonce, data, None)
|
||||
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
|
||||
ciphertext = self.cipher.encrypt(self.nonce, data, None)
|
||||
self._increment_nonce()
|
||||
return ciphertext
|
||||
|
||||
def decrypt_data(self, data: bytes) -> Any:
|
||||
return self.cipher.decrypt(self.nonce, data, None)
|
||||
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
|
||||
plaintext = self.cipher.decrypt(self.nonce, data, None)
|
||||
self._increment_nonce()
|
||||
return plaintext
|
||||
|
Reference in New Issue
Block a user