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
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
Reference in New Issue
Block a user