e523398fa8
- 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
280 lines
9.5 KiB
C
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;
|
|
}
|