Files
Nexus-Guard-Suite/apps/device-agent-embedded/main/provision.c
T
datadunia e523398fa8 feat: add ESP32 embedded agent + namespace VRF reference docs + update android-agent submodule
- apps/device-agent-embedded/: ESP32 WireGuard agent (C, ESP-IDF, mbedTLS)
- reference/namespace-vrf/: Linux namespace/VRF network isolation docs
- apps/android-agent: VPN permission check, StartResult sealed class
2026-09-13 14:39:24 +07:00

280 lines
9.5 KiB
C

/**
* @file provision.c
* @brief HTTP Provisioning
*/
#include <string.h>
#include <stdlib.h>
#include "provision.h"
#include "config.h"
#include "crypto.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "cJSON.h"
#include "mbedtls/base64.h"
static const char *TAG = "nexus_provision";
/* Retry config */
#define MAX_RETRIES 3
#define CONNECT_TIMEOUT_MS 10000
#define READ_TIMEOUT_MS 30000
/* Config key name for decryption (must match server) */
#define CONFIG_NEXUS_SALT CONFIG_NEXUS_SERVER_SALT
/**
* @brief Helper: copy cJSON string to char buffer with bounds check
*/
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
if (cJSON_IsString(item) && item->valuestring != NULL) {
strncpy(buf, item->valuestring, buf_len - 1);
buf[buf_len - 1] = '\0';
}
}
/**
* @brief HTTP event handler for provisioning
*/
static esp_err_t provision_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
break;
case HTTP_EVENT_ON_HEADER:
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
esp_err_t provision_device(const char *server_url, const char *token,
const char *hwid, wg_config_t *config)
{
if (server_url == NULL || token == NULL || hwid == NULL || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Starting provisioning...");
/* Build request JSON */
cJSON *req_json = cJSON_CreateObject();
cJSON_AddStringToObject(req_json, "token", token);
cJSON_AddStringToObject(req_json, "hwid", hwid);
char *req_body = cJSON_PrintUnformatted(req_json);
cJSON_Delete(req_json);
if (req_body == NULL) {
ESP_LOGE(TAG, "Failed to create request JSON");
return ESP_ERR_NO_MEM;
}
/* Build URL */
char url[192];
snprintf(url, sizeof(url), "%s/api/v1/provision", server_url);
esp_err_t final_err = ESP_FAIL;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
ESP_LOGI(TAG, "Provisioning attempt %d/%d...", attempt, MAX_RETRIES);
/* Configure HTTP client */
esp_http_client_config_t http_config = {
.url = url,
.event_handler = provision_event_handler,
.timeout_ms = CONNECT_TIMEOUT_MS,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.skip_cert_common_name_check = true, /* TODO: use proper CA cert */
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
ESP_LOGE(TAG, "Failed to init HTTP client");
continue;
}
/* Set POST data */
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, req_body, strlen(req_body));
/* Execute request */
esp_err_t err = esp_http_client_open(client, strlen(req_body));
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int written = esp_http_client_write(client, req_body, strlen(req_body));
if (written < 0) {
ESP_LOGE(TAG, "HTTP write failed");
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGE(TAG, "Provisioning failed, HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Read response body */
char *response_buf = malloc(content_length + 1);
if (response_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate response buffer");
esp_http_client_close(client);
esp_http_client_cleanup(client);
final_err = ESP_ERR_NO_MEM;
break;
}
int read_len = esp_http_client_read(client, response_buf, content_length);
esp_http_client_close(client);
esp_http_client_cleanup(client);
if (read_len < 0) {
ESP_LOGE(TAG, "HTTP read failed");
free(response_buf);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
response_buf[read_len] = '\0';
/* Parse response JSON */
cJSON *resp_json = cJSON_Parse(response_buf);
free(response_buf);
if (resp_json == NULL) {
ESP_LOGE(TAG, "Failed to parse response JSON");
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Extract encrypted_config (base64) */
cJSON *encrypted_item = cJSON_GetObjectItemCaseSensitive(resp_json, "encrypted_config");
if (!cJSON_IsString(encrypted_item) || encrypted_item->valuestring == NULL) {
ESP_LOGE(TAG, "Response missing 'encrypted_config'");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
const char *b64_data = encrypted_item->valuestring;
size_t b64_len = strlen(b64_data);
/* Base64 decode */
size_t decoded_max_len = (b64_len * 3) / 4 + 4;
uint8_t *decoded_buf = malloc(decoded_max_len);
if (decoded_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate base64 decode buffer");
cJSON_Delete(resp_json);
final_err = ESP_ERR_NO_MEM;
break;
}
size_t decoded_len = 0;
int b64_ret = mbedtls_base64_decode(decoded_buf, decoded_max_len, &decoded_len,
(const unsigned char *)b64_data, b64_len);
if (b64_ret != 0) {
ESP_LOGE(TAG, "Base64 decode failed: -0x%04x", -b64_ret);
free(decoded_buf);
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Derive decryption key: SHA256(hwid + salt) */
uint8_t key[32];
err = crypto_derive_key(hwid, CONFIG_NEXUS_SALT, key);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Key derivation failed");
explicit_bzero(key, sizeof(key));
free(decoded_buf);
cJSON_Delete(resp_json);
final_err = err;
break;
}
/* Decrypt config */
uint8_t *plaintext = NULL;
size_t plaintext_len = 0;
err = crypto_decrypt(decoded_buf, decoded_len, key, &plaintext, &plaintext_len);
explicit_bzero(key, sizeof(key));
free(decoded_buf);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Decryption failed");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Parse decrypted JSON payload */
cJSON *config_json = cJSON_Parse((char *)plaintext);
free(plaintext);
if (config_json == NULL) {
ESP_LOGE(TAG, "Failed to parse decrypted config JSON");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Extract config fields */
copy_json_str(config_json, "device_id", config->device_id, sizeof(config->device_id));
copy_json_str(config_json, "private_key", config->private_key, sizeof(config->private_key));
copy_json_str(config_json, "preshared_key", config->preshared_key, sizeof(config->preshared_key));
copy_json_str(config_json, "internal_ip", config->internal_ip, sizeof(config->internal_ip));
copy_json_str(config_json, "server_pub", config->server_pub, sizeof(config->server_pub));
copy_json_str(config_json, "endpoint", config->endpoint, sizeof(config->endpoint));
copy_json_str(config_json, "dns", config->dns, sizeof(config->dns));
copy_json_str(config_json, "allowed_ips", config->allowed_ips, sizeof(config->allowed_ips));
copy_json_str(config_json, "server_wg_ip", config->server_wg_ip, sizeof(config->server_wg_ip));
copy_json_str(config_json, "config_hash", config->config_hash, sizeof(config->config_hash));
cJSON_Delete(config_json);
cJSON_Delete(resp_json);
/* Save config to NVS */
err = config_save(config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err));
final_err = err;
break;
}
ESP_LOGI(TAG, "Provisioning complete, device: %s", config->device_id);
final_err = ESP_OK;
break;
}
free(req_body);
return final_err;
}