Add Authenticated HTTPS Admin Foundation
This commit is contained in:
@@ -4,6 +4,7 @@ idf_component_register(
|
||||
"console_completion.c"
|
||||
"network_console.c"
|
||||
"system_console.c"
|
||||
"secure_random.c"
|
||||
"status_led.c"
|
||||
"rs232_hw_test.c"
|
||||
"rs232_port_owner.c"
|
||||
@@ -14,6 +15,9 @@ idf_component_register(
|
||||
"session_console.c"
|
||||
"usb_cdc_transport.c"
|
||||
"usb_console.c"
|
||||
"web_security.c"
|
||||
"web_server.c"
|
||||
"web_console.c"
|
||||
"wifi_config.c"
|
||||
"wifi_manager.c"
|
||||
"wifi_console.c"
|
||||
@@ -24,6 +28,8 @@ idf_component_register(
|
||||
esp_driver_gpio
|
||||
esp_driver_uart
|
||||
esp_event
|
||||
esp_http_server
|
||||
esp_https_server
|
||||
esp_netif
|
||||
esp_psram
|
||||
esp_system
|
||||
|
||||
@@ -108,6 +108,24 @@ static const char *const s_completion_candidates[] = {
|
||||
"wifi ping",
|
||||
"wifi nslookup",
|
||||
"wifi traceroute",
|
||||
|
||||
/* Authenticated HTTPS lifecycle and physical-admin recovery operations. */
|
||||
"web help",
|
||||
"web status",
|
||||
"web start",
|
||||
"web stop",
|
||||
"web counters",
|
||||
"web clear-counters",
|
||||
"web credentials",
|
||||
"web credentials show",
|
||||
"web credentials rotate",
|
||||
"web credentials rotate --force",
|
||||
"web certificate",
|
||||
"web certificate info",
|
||||
"web certificate rotate",
|
||||
"web certificate rotate --force",
|
||||
"web reset",
|
||||
"web reset --force",
|
||||
};
|
||||
|
||||
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
|
||||
|
||||
+47
-5
@@ -7,6 +7,7 @@
|
||||
#include "network_console.h"
|
||||
#include "rs232_hw_test.h"
|
||||
#include "rs232_port_owner.h"
|
||||
#include "secure_random.h"
|
||||
#include "serial_config.h"
|
||||
#include "serial_console.h"
|
||||
#include "serial_service.h"
|
||||
@@ -16,6 +17,9 @@
|
||||
#include "system_console.h"
|
||||
#include "usb_cdc_transport.h"
|
||||
#include "usb_console.h"
|
||||
#include "web_console.h"
|
||||
#include "web_security.h"
|
||||
#include "web_server.h"
|
||||
#include "wifi_config.h"
|
||||
#include "wifi_console.h"
|
||||
#include "wifi_manager.h"
|
||||
@@ -28,7 +32,7 @@ static const char *TAG = "firmware";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Wi-Fi foundation phase started");
|
||||
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife HTTPS foundation phase started");
|
||||
|
||||
if (esp_psram_is_initialized()) {
|
||||
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
|
||||
@@ -36,8 +40,12 @@ void app_main(void)
|
||||
ESP_LOGW(TAG, "PSRAM is not initialized");
|
||||
}
|
||||
|
||||
/* Seed credential generation before any future RF or ADC initialization. */
|
||||
esp_err_t wifi_entropy_error = wifi_config_entropy_init();
|
||||
/* Seed the sole device DRBG before any future RF, Bluetooth, or ADC use. */
|
||||
esp_err_t random_error = secure_random_init();
|
||||
if (random_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Secure random initialization failed: %s",
|
||||
esp_err_to_name(random_error));
|
||||
}
|
||||
|
||||
/* Blue means the firmware is initialized and waiting for a console command. */
|
||||
ESP_ERROR_CHECK(status_led_init());
|
||||
@@ -59,9 +67,30 @@ void app_main(void)
|
||||
/* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */
|
||||
ESP_ERROR_CHECK(usb_cdc_transport_init());
|
||||
|
||||
/* Provision HTTPS identity before Wi-Fi starts; failures leave UART/USB recovery intact. */
|
||||
web_security_load_result_t web_security_source = WEB_SECURITY_LOAD_STORED;
|
||||
esp_err_t web_security_error = random_error;
|
||||
if (web_security_error == ESP_OK) {
|
||||
web_security_error = web_security_init(&web_security_source);
|
||||
}
|
||||
if (web_security_error != ESP_OK) {
|
||||
ESP_LOGE(TAG,
|
||||
"HTTPS security material unavailable (%s); use UART0 'web reset --force' to replace it",
|
||||
esp_err_to_name(web_security_error));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Using %s HTTPS identity and administrative credential",
|
||||
web_security_source == WEB_SECURITY_LOAD_STORED ? "stored" : "newly generated");
|
||||
}
|
||||
|
||||
esp_err_t web_runtime_error = web_server_init();
|
||||
if (web_runtime_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTPS runtime initialization failed: %s",
|
||||
esp_err_to_name(web_runtime_error));
|
||||
}
|
||||
|
||||
wifi_app_config_t wifi_config;
|
||||
wifi_config_load_source_t wifi_config_source;
|
||||
esp_err_t wifi_config_error = wifi_entropy_error;
|
||||
esp_err_t wifi_config_error = random_error;
|
||||
if (wifi_config_error == ESP_OK) {
|
||||
wifi_config_error = wifi_config_load(&wifi_config, &wifi_config_source);
|
||||
}
|
||||
@@ -82,8 +111,9 @@ void app_main(void)
|
||||
"Stored Wi-Fi configuration is incompatible; using RAM defaults without overwriting it");
|
||||
}
|
||||
|
||||
esp_err_t wifi_error = wifi_config_error;
|
||||
if (wifi_config_error == ESP_OK) {
|
||||
esp_err_t wifi_error = wifi_manager_init(&wifi_config);
|
||||
wifi_error = wifi_manager_init(&wifi_config);
|
||||
if (wifi_error == ESP_OK && wifi_config.enabled_at_boot != 0U) {
|
||||
wifi_error = wifi_manager_start();
|
||||
}
|
||||
@@ -103,6 +133,17 @@ void app_main(void)
|
||||
}
|
||||
wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config));
|
||||
|
||||
if (wifi_error == ESP_OK && web_security_error == ESP_OK &&
|
||||
web_runtime_error == ESP_OK) {
|
||||
esp_err_t start_error = web_server_start();
|
||||
if (start_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTPS startup failed: %s; UART0 recovery remains available",
|
||||
esp_err_to_name(start_error));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Authenticated HTTPS listening on TCP port 443");
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(
|
||||
TAG,
|
||||
"Using %s serial configuration; UART service starts on 'serial start' or native USB open",
|
||||
@@ -132,6 +173,7 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(session_console_register_commands());
|
||||
ESP_ERROR_CHECK(usb_console_register_commands());
|
||||
ESP_ERROR_CHECK(wifi_console_register_commands());
|
||||
ESP_ERROR_CHECK(web_console_register_commands());
|
||||
ESP_ERROR_CHECK(network_console_register_root_commands());
|
||||
ESP_ERROR_CHECK(system_console_register_commands());
|
||||
/* Upgrade late UART terminals safely and add nested completion. */
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Pre-radio seeding and serialized access to the device-wide CTR_DRBG. */
|
||||
|
||||
#include "secure_random.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bootloader_random.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "mbedtls/ctr_drbg.h"
|
||||
|
||||
static SemaphoreHandle_t s_random_mutex;
|
||||
static mbedtls_ctr_drbg_context s_drbg;
|
||||
static bool s_drbg_ready;
|
||||
static uint32_t s_generate_calls;
|
||||
|
||||
static int pre_radio_entropy(void *context, unsigned char *output, size_t length)
|
||||
{
|
||||
(void)context;
|
||||
|
||||
/* This callback is deliberately reachable only from secure_random_init(). */
|
||||
bootloader_random_enable();
|
||||
esp_fill_random(output, length);
|
||||
bootloader_random_disable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t secure_random_init(void)
|
||||
{
|
||||
static const unsigned char personalization[] =
|
||||
"esp32-serial-swiss-army-knife";
|
||||
|
||||
if (s_drbg_ready) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (s_random_mutex == NULL) {
|
||||
s_random_mutex = xSemaphoreCreateMutex();
|
||||
if (s_random_mutex == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_random_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_OK;
|
||||
if (!s_drbg_ready) {
|
||||
mbedtls_ctr_drbg_init(&s_drbg);
|
||||
int result = mbedtls_ctr_drbg_seed(&s_drbg,
|
||||
pre_radio_entropy,
|
||||
NULL,
|
||||
personalization,
|
||||
sizeof(personalization) - 1U);
|
||||
if (result == 0) {
|
||||
/* Reseeding would re-enter a pre-radio-only entropy path after RF starts. */
|
||||
mbedtls_ctr_drbg_set_reseed_interval(&s_drbg, INT_MAX);
|
||||
s_generate_calls = 0U;
|
||||
s_drbg_ready = true;
|
||||
} else {
|
||||
mbedtls_ctr_drbg_free(&s_drbg);
|
||||
error = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_random_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t secure_random_fill(void *output, size_t length)
|
||||
{
|
||||
if (length == 0U) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (output == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!s_drbg_ready || s_random_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
unsigned char *cursor = (unsigned char *)output;
|
||||
esp_err_t error = ESP_OK;
|
||||
|
||||
xSemaphoreTake(s_random_mutex, portMAX_DELAY);
|
||||
while (length > 0U) {
|
||||
/* CTR_DRBG limits each request even though the public API need not. */
|
||||
size_t chunk = length;
|
||||
if (chunk > MBEDTLS_CTR_DRBG_MAX_REQUEST) {
|
||||
chunk = MBEDTLS_CTR_DRBG_MAX_REQUEST;
|
||||
}
|
||||
/*
|
||||
* Mbed TLS stores its reseed counter in a signed int. Fail closed one
|
||||
* call before INT_MAX so it can neither overflow nor invoke the
|
||||
* pre-radio-only entropy callback during the device's lifetime.
|
||||
*/
|
||||
if (s_generate_calls >= (uint32_t)INT_MAX - 1U) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
break;
|
||||
}
|
||||
if (mbedtls_ctr_drbg_random(&s_drbg, cursor, chunk) != 0) {
|
||||
error = ESP_FAIL;
|
||||
break;
|
||||
}
|
||||
++s_generate_calls;
|
||||
cursor += chunk;
|
||||
length -= chunk;
|
||||
}
|
||||
xSemaphoreGive(s_random_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
int secure_random_mbedtls(void *context, unsigned char *output, size_t length)
|
||||
{
|
||||
(void)context;
|
||||
return secure_random_fill(output, length) == ESP_OK
|
||||
? 0
|
||||
: MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED;
|
||||
}
|
||||
|
||||
void secure_wipe(void *data, size_t size)
|
||||
{
|
||||
volatile uint8_t *byte = (volatile uint8_t *)data;
|
||||
|
||||
if (byte == NULL) {
|
||||
return;
|
||||
}
|
||||
while (size-- > 0U) {
|
||||
*byte++ = 0U;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Device-wide cryptographic random generator shared by security subsystems. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Seed the sole CTR_DRBG while bootloader entropy is safe to enable. Call this
|
||||
* before Wi-Fi, Bluetooth, or ADC startup; later calls are idempotent and do
|
||||
* not touch the hardware entropy source.
|
||||
*/
|
||||
esp_err_t secure_random_init(void);
|
||||
|
||||
/* Fill output from the already-seeded, mutex-protected device DRBG. */
|
||||
esp_err_t secure_random_fill(void *output, size_t length);
|
||||
|
||||
/* Mbed TLS-compatible adapter: zero means success, negative means failure. */
|
||||
int secure_random_mbedtls(void *context, unsigned char *output, size_t length);
|
||||
|
||||
/* Volatile stores keep cleanup of key material from being optimized away. */
|
||||
void secure_wipe(void *data, size_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,303 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* UART0 HTTPS lifecycle, credentials, certificate, and recovery commands. */
|
||||
|
||||
#include "web_console.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_console.h"
|
||||
#include "secure_random.h"
|
||||
#include "web_security.h"
|
||||
#include "web_server.h"
|
||||
|
||||
static void print_usage(void)
|
||||
{
|
||||
printf("Usage:\n");
|
||||
printf(" web status|start|stop\n");
|
||||
printf(" web counters|clear-counters\n");
|
||||
printf(" web credentials show\n");
|
||||
printf(" web credentials rotate --force\n");
|
||||
printf(" web certificate info\n");
|
||||
printf(" web certificate rotate --force\n");
|
||||
printf(" web reset --force\n");
|
||||
}
|
||||
|
||||
static void print_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH])
|
||||
{
|
||||
for (size_t index = 0U; index < WEB_SECURITY_SHA256_LENGTH; ++index) {
|
||||
printf(index == 0U ? "%02X" : ":%02X", (unsigned int)fingerprint[index]);
|
||||
}
|
||||
}
|
||||
|
||||
static int show_status(void)
|
||||
{
|
||||
web_server_snapshot_t snapshot;
|
||||
esp_err_t error = web_server_get_snapshot(&snapshot);
|
||||
if (error != ESP_OK) {
|
||||
printf("HTTPS runtime unavailable: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
char username[WEB_SECURITY_USERNAME_CAPACITY + 1U] = {0};
|
||||
size_t username_length = 0U;
|
||||
esp_err_t security_error = web_security_copy_username(
|
||||
username, sizeof(username), &username_length);
|
||||
printf("HTTPS: initialized=%s running=%s transitioning=%s port=%u last-error=%s\n",
|
||||
snapshot.initialized ? "yes" : "no",
|
||||
snapshot.running ? "yes" : "no",
|
||||
snapshot.transitioning ? "yes" : "no",
|
||||
(unsigned int)snapshot.port,
|
||||
esp_err_to_name(snapshot.last_error));
|
||||
if (security_error == ESP_OK) {
|
||||
printf("Authentication: HTTP Basic over TLS, username=%.*s, material=ready\n",
|
||||
(int)username_length, username);
|
||||
} else {
|
||||
printf("Authentication material unavailable: %s; use 'web reset --force' to replace it.\n",
|
||||
esp_err_to_name(security_error));
|
||||
}
|
||||
printf("Endpoints: GET / and GET /api/status (authentication required)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int show_counters(void)
|
||||
{
|
||||
web_server_snapshot_t snapshot;
|
||||
esp_err_t error = web_server_get_snapshot(&snapshot);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not read HTTPS counters: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const web_server_counters_t *counter = &snapshot.counters;
|
||||
printf("Lifecycle: starts=%" PRIu64 " start-failures=%" PRIu64
|
||||
" stops=%" PRIu64 "\n",
|
||||
counter->starts, counter->start_failures, counter->stops);
|
||||
printf("Requests: total=%" PRIu64 " authenticated=%" PRIu64
|
||||
" auth-failures=%" PRIu64 " root=%" PRIu64
|
||||
" status=%" PRIu64 " response-errors=%" PRIu64 "\n",
|
||||
counter->requests, counter->authenticated_requests,
|
||||
counter->authentication_failures, counter->root_requests,
|
||||
counter->status_requests, counter->response_errors);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int show_credentials(void)
|
||||
{
|
||||
web_security_credentials_t credentials;
|
||||
esp_err_t error = web_security_show_credentials(&credentials);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not read web credentials: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Username: %.*s\n", (int)credentials.username_length,
|
||||
credentials.username);
|
||||
printf("Password: %.*s\n", (int)credentials.password_length,
|
||||
credentials.password);
|
||||
printf("These credentials protect HTTPS only. Keep them private.\n");
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int show_certificate(void)
|
||||
{
|
||||
web_security_certificate_metadata_t metadata;
|
||||
esp_err_t error = web_security_get_certificate_metadata(&metadata);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not read certificate information: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Security material generation=%" PRIu32 " type=ECDSA-P256 signature=SHA-256\n",
|
||||
metadata.material_generation);
|
||||
printf("Subject/issuer CN: %s\n", metadata.common_name);
|
||||
printf("Validity: %sZ to %sZ\n", metadata.not_before, metadata.not_after);
|
||||
printf("SAN: DNS:%s, IP:%u.%u.%u.%u\n",
|
||||
metadata.dns_name,
|
||||
(unsigned int)metadata.ipv4_address[0],
|
||||
(unsigned int)metadata.ipv4_address[1],
|
||||
(unsigned int)metadata.ipv4_address[2],
|
||||
(unsigned int)metadata.ipv4_address[3]);
|
||||
printf("SHA-256 fingerprint: ");
|
||||
print_fingerprint(metadata.sha256_fingerprint);
|
||||
putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool force_is_present(int argc, char **argv, int expected_argc)
|
||||
{
|
||||
return argc == expected_argc && strcmp(argv[expected_argc - 1], "--force") == 0;
|
||||
}
|
||||
|
||||
static int restart_if_running(bool was_running)
|
||||
{
|
||||
if (!was_running) {
|
||||
return 0;
|
||||
}
|
||||
esp_err_t error = web_server_stop();
|
||||
if (error != ESP_OK) {
|
||||
printf("Material changed, but the old TLS server could not stop: %s\n",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
error = web_server_start();
|
||||
if (error != ESP_OK) {
|
||||
printf("Material changed, but HTTPS could not restart: %s\n",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rotate_credentials(void)
|
||||
{
|
||||
web_security_credentials_t credentials;
|
||||
esp_err_t error = web_security_rotate_credentials(&credentials);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not rotate web credentials: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Web credentials rotated and persisted. Existing Basic credentials are now invalid.\n");
|
||||
printf("Username: %.*s\nPassword: %.*s\n",
|
||||
(int)credentials.username_length, credentials.username,
|
||||
(int)credentials.password_length, credentials.password);
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rotate_certificate(void)
|
||||
{
|
||||
web_server_snapshot_t snapshot;
|
||||
esp_err_t error = web_server_get_snapshot(&snapshot);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not inspect HTTPS runtime: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
error = web_security_rotate_certificate();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not rotate web certificate: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("Web certificate and private key rotated and persisted.\n");
|
||||
return restart_if_running(snapshot.running);
|
||||
}
|
||||
|
||||
static int reset_material(void)
|
||||
{
|
||||
web_server_snapshot_t snapshot;
|
||||
bool was_running = web_server_get_snapshot(&snapshot) == ESP_OK && snapshot.running;
|
||||
web_security_credentials_t credentials;
|
||||
esp_err_t error = web_security_reset_all(&credentials);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not reset web security material: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Web credentials, certificate, and private key replaced and persisted.\n");
|
||||
printf("Username: %.*s\nPassword: %.*s\n",
|
||||
(int)credentials.username_length, credentials.username,
|
||||
(int)credentials.password_length, credentials.password);
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
if (was_running) {
|
||||
return restart_if_running(true);
|
||||
}
|
||||
|
||||
error = web_server_start();
|
||||
if (error != ESP_OK) {
|
||||
printf("Security material recovered, but HTTPS could not start: %s\n",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("HTTPS started with the recovered security material.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int command_web(int argc, char **argv)
|
||||
{
|
||||
if (argc == 1 || (argc == 2 && strcmp(argv[1], "help") == 0)) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && strcmp(argv[1], "status") == 0) {
|
||||
return show_status();
|
||||
}
|
||||
if (argc == 2 && strcmp(argv[1], "start") == 0) {
|
||||
esp_err_t error = web_server_start();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not start HTTPS: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("HTTPS started on TCP port 443.\n");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
|
||||
esp_err_t error = web_server_stop();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not stop HTTPS: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("HTTPS stopped.\n");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
|
||||
return show_counters();
|
||||
}
|
||||
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
|
||||
esp_err_t error = web_server_clear_counters();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not clear HTTPS counters: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("HTTPS counters cleared.\n");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 3 && strcmp(argv[1], "credentials") == 0 &&
|
||||
strcmp(argv[2], "show") == 0) {
|
||||
return show_credentials();
|
||||
}
|
||||
if (strcmp(argv[1], "credentials") == 0 && argc >= 3 &&
|
||||
strcmp(argv[2], "rotate") == 0) {
|
||||
if (!force_is_present(argc, argv, 4)) {
|
||||
printf("Credential rotation requires: web credentials rotate --force\n");
|
||||
return 1;
|
||||
}
|
||||
return rotate_credentials();
|
||||
}
|
||||
if (argc == 3 && strcmp(argv[1], "certificate") == 0 &&
|
||||
strcmp(argv[2], "info") == 0) {
|
||||
return show_certificate();
|
||||
}
|
||||
if (strcmp(argv[1], "certificate") == 0 && argc >= 3 &&
|
||||
strcmp(argv[2], "rotate") == 0) {
|
||||
if (!force_is_present(argc, argv, 4)) {
|
||||
printf("Certificate rotation requires: web certificate rotate --force\n");
|
||||
return 1;
|
||||
}
|
||||
return rotate_certificate();
|
||||
}
|
||||
if (strcmp(argv[1], "reset") == 0) {
|
||||
if (!force_is_present(argc, argv, 3)) {
|
||||
printf("Full material replacement requires: web reset --force\n");
|
||||
return 1;
|
||||
}
|
||||
return reset_material();
|
||||
}
|
||||
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
esp_err_t web_console_register_commands(void)
|
||||
{
|
||||
const esp_console_cmd_t command = {
|
||||
.command = "web",
|
||||
.help = "Manage authenticated HTTPS and recover web credentials/certificate",
|
||||
.hint = NULL,
|
||||
.func = &command_web,
|
||||
.argtable = NULL,
|
||||
};
|
||||
return esp_console_cmd_register(&command);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Physical UART0 administration commands for HTTPS recovery. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t web_console_register_commands(void);
|
||||
@@ -0,0 +1,979 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Canonical NVS storage for HTTPS identity and administrative credentials. */
|
||||
|
||||
#include "web_security.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_mac.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "mbedtls/asn1.h"
|
||||
#include "mbedtls/ecp.h"
|
||||
#include "mbedtls/oid.h"
|
||||
#include "mbedtls/pk.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
#include "mbedtls/x509_crt.h"
|
||||
#include "nvs.h"
|
||||
#include "secure_random.h"
|
||||
|
||||
#define WEB_SECURITY_SCHEMA_VERSION 1U
|
||||
#define WEB_SECURITY_BLOB_SIZE 1392U
|
||||
|
||||
static const uint8_t s_admin_username[] = "admin";
|
||||
static const char s_password_alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
static const uint8_t s_ap_ipv4_address[4] = {192U, 168U, 4U, 1U};
|
||||
|
||||
typedef struct {
|
||||
uint32_t schema_version;
|
||||
uint16_t blob_size;
|
||||
uint16_t reserved_header;
|
||||
uint32_t generation;
|
||||
uint8_t username_length;
|
||||
uint8_t password_length;
|
||||
uint16_t private_key_length;
|
||||
uint16_t certificate_length;
|
||||
uint16_t reserved_lengths;
|
||||
uint8_t username[WEB_SECURITY_USERNAME_CAPACITY];
|
||||
uint8_t password[WEB_SECURITY_PASSWORD_CAPACITY];
|
||||
uint8_t private_key_der[WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY];
|
||||
uint8_t certificate_der[WEB_SECURITY_CERTIFICATE_DER_CAPACITY];
|
||||
uint8_t certificate_fingerprint[WEB_SECURITY_SHA256_LENGTH];
|
||||
uint8_t reserved[12];
|
||||
} web_security_blob_t;
|
||||
|
||||
_Static_assert(offsetof(web_security_blob_t, username) == 20U,
|
||||
"web security schema offsets changed");
|
||||
_Static_assert(offsetof(web_security_blob_t, private_key_der) == 68U,
|
||||
"web security key offset changed");
|
||||
_Static_assert(offsetof(web_security_blob_t, certificate_der) == 324U,
|
||||
"web security certificate offset changed");
|
||||
_Static_assert(sizeof(web_security_blob_t) == WEB_SECURITY_BLOB_SIZE,
|
||||
"web security schema size changed");
|
||||
|
||||
static SemaphoreHandle_t s_security_mutex;
|
||||
static web_security_blob_t s_material;
|
||||
static bool s_material_ready;
|
||||
static web_security_load_result_t s_load_result;
|
||||
|
||||
static bool bytes_are_zero(const uint8_t *data, size_t size)
|
||||
{
|
||||
for (size_t i = 0U; i < size; ++i) {
|
||||
if (data[i] != 0U) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unused_bytes_are_zero(const uint8_t *data, size_t used, size_t capacity)
|
||||
{
|
||||
return used <= capacity && bytes_are_zero(data + used, capacity - used);
|
||||
}
|
||||
|
||||
static bool constant_time_equal(const uint8_t *left, const uint8_t *right, size_t size)
|
||||
{
|
||||
uint8_t difference = 0U;
|
||||
for (size_t i = 0U; i < size; ++i) {
|
||||
difference |= left[i] ^ right[i];
|
||||
}
|
||||
return difference == 0U;
|
||||
}
|
||||
|
||||
static esp_err_t ensure_security_mutex(void)
|
||||
{
|
||||
if (s_security_mutex != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
s_security_mutex = xSemaphoreCreateMutex();
|
||||
return s_security_mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
static esp_err_t build_device_names(char *common_name, size_t common_name_size,
|
||||
char *dns_name, size_t dns_name_size)
|
||||
{
|
||||
uint8_t mac[6] = {0};
|
||||
esp_err_t error = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
int common_length = snprintf(common_name, common_name_size,
|
||||
"ESP32 SAK %02X%02X%02X",
|
||||
mac[3], mac[4], mac[5]);
|
||||
int dns_length = snprintf(dns_name, dns_name_size,
|
||||
"esp32-sak-%02x%02x%02x.local",
|
||||
mac[3], mac[4], mac[5]);
|
||||
if (common_length < 0 || (size_t)common_length >= common_name_size ||
|
||||
dns_length < 0 || (size_t)dns_length >= dns_name_size) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t generate_credentials(web_security_blob_t *blob)
|
||||
{
|
||||
uint8_t random_bytes[WEB_SECURITY_PASSWORD_LENGTH] = {0};
|
||||
|
||||
memset(blob->username, 0, sizeof(blob->username));
|
||||
memset(blob->password, 0, sizeof(blob->password));
|
||||
memcpy(blob->username, s_admin_username, sizeof(s_admin_username) - 1U);
|
||||
blob->username_length = sizeof(s_admin_username) - 1U;
|
||||
blob->password_length = WEB_SECURITY_PASSWORD_LENGTH;
|
||||
|
||||
esp_err_t error = secure_random_fill(random_bytes, sizeof(random_bytes));
|
||||
if (error == ESP_OK) {
|
||||
/* Sixty-four symbols consume six random bits exactly, without modulo bias. */
|
||||
for (size_t i = 0U; i < sizeof(random_bytes); ++i) {
|
||||
blob->password[i] =
|
||||
(uint8_t)s_password_alphabet[random_bytes[i] & 0x3fU];
|
||||
}
|
||||
}
|
||||
secure_wipe(random_bytes, sizeof(random_bytes));
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t normalize_der(unsigned char *buffer, size_t capacity,
|
||||
int written, uint16_t *output_length)
|
||||
{
|
||||
if (written <= 0 || (size_t)written > capacity || written > UINT16_MAX) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Mbed TLS DER writers grow backward from the end of the output buffer. */
|
||||
memmove(buffer, buffer + capacity - (size_t)written, (size_t)written);
|
||||
memset(buffer + (size_t)written, 0, capacity - (size_t)written);
|
||||
*output_length = (uint16_t)written;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t generate_certificate(web_security_blob_t *blob)
|
||||
{
|
||||
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY] = {0};
|
||||
char dns_name[WEB_SECURITY_DNS_NAME_CAPACITY] = {0};
|
||||
char distinguished_name[WEB_SECURITY_COMMON_NAME_CAPACITY + 3U] = {0};
|
||||
unsigned char serial[16] = {0};
|
||||
mbedtls_pk_context key;
|
||||
mbedtls_x509write_cert writer;
|
||||
esp_err_t error = ESP_FAIL;
|
||||
int result;
|
||||
|
||||
mbedtls_pk_init(&key);
|
||||
mbedtls_x509write_crt_init(&writer);
|
||||
memset(blob->private_key_der, 0, sizeof(blob->private_key_der));
|
||||
memset(blob->certificate_der, 0, sizeof(blob->certificate_der));
|
||||
memset(blob->certificate_fingerprint, 0, sizeof(blob->certificate_fingerprint));
|
||||
blob->private_key_length = 0U;
|
||||
blob->certificate_length = 0U;
|
||||
|
||||
error = build_device_names(common_name, sizeof(common_name),
|
||||
dns_name, sizeof(dns_name));
|
||||
if (error != ESP_OK) {
|
||||
goto cleanup;
|
||||
}
|
||||
result = snprintf(distinguished_name, sizeof(distinguished_name),
|
||||
"CN=%s", common_name);
|
||||
if (result < 0 || (size_t)result >= sizeof(distinguished_name)) {
|
||||
error = ESP_ERR_INVALID_SIZE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
result = mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
|
||||
if (result != 0) {
|
||||
error = ESP_ERR_NO_MEM;
|
||||
goto cleanup;
|
||||
}
|
||||
result = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1,
|
||||
mbedtls_pk_ec(key),
|
||||
secure_random_mbedtls, NULL);
|
||||
if (result != 0) {
|
||||
error = ESP_FAIL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
result = mbedtls_pk_write_key_der(&key, blob->private_key_der,
|
||||
sizeof(blob->private_key_der));
|
||||
error = normalize_der(blob->private_key_der, sizeof(blob->private_key_der),
|
||||
result, &blob->private_key_length);
|
||||
if (error != ESP_OK) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error = secure_random_fill(serial, sizeof(serial));
|
||||
if (error != ESP_OK) {
|
||||
goto cleanup;
|
||||
}
|
||||
/* RFC 5280 serials are positive and nonzero; retain 127 random bits. */
|
||||
serial[0] &= 0x7fU;
|
||||
serial[0] |= 0x01U;
|
||||
|
||||
mbedtls_x509_san_list dns_san = {0};
|
||||
mbedtls_x509_san_list ip_san = {0};
|
||||
dns_san.node.type = MBEDTLS_X509_SAN_DNS_NAME;
|
||||
dns_san.node.san.unstructured_name.p = (unsigned char *)dns_name;
|
||||
dns_san.node.san.unstructured_name.len = strlen(dns_name);
|
||||
dns_san.next = &ip_san;
|
||||
ip_san.node.type = MBEDTLS_X509_SAN_IP_ADDRESS;
|
||||
ip_san.node.san.unstructured_name.p = (unsigned char *)s_ap_ipv4_address;
|
||||
ip_san.node.san.unstructured_name.len = sizeof(s_ap_ipv4_address);
|
||||
|
||||
mbedtls_asn1_sequence server_auth = {0};
|
||||
server_auth.buf.tag = MBEDTLS_ASN1_OID;
|
||||
server_auth.buf.p = (unsigned char *)MBEDTLS_OID_SERVER_AUTH;
|
||||
server_auth.buf.len = MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH);
|
||||
|
||||
mbedtls_x509write_crt_set_version(&writer, MBEDTLS_X509_CRT_VERSION_3);
|
||||
mbedtls_x509write_crt_set_md_alg(&writer, MBEDTLS_MD_SHA256);
|
||||
mbedtls_x509write_crt_set_subject_key(&writer, &key);
|
||||
mbedtls_x509write_crt_set_issuer_key(&writer, &key);
|
||||
|
||||
result = mbedtls_x509write_crt_set_serial_raw(&writer, serial, sizeof(serial));
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_validity(
|
||||
&writer, WEB_SECURITY_CERT_NOT_BEFORE, WEB_SECURITY_CERT_NOT_AFTER);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_subject_name(&writer, distinguished_name);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_issuer_name(&writer, distinguished_name);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_basic_constraints(&writer, 0, -1);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_key_usage(
|
||||
&writer, MBEDTLS_X509_KU_DIGITAL_SIGNATURE);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_ext_key_usage(&writer, &server_auth);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = mbedtls_x509write_crt_set_subject_alternative_name(&writer,
|
||||
&dns_san);
|
||||
}
|
||||
if (result != 0) {
|
||||
error = ESP_FAIL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
result = mbedtls_x509write_crt_der(&writer, blob->certificate_der,
|
||||
sizeof(blob->certificate_der),
|
||||
secure_random_mbedtls, NULL);
|
||||
error = normalize_der(blob->certificate_der, sizeof(blob->certificate_der),
|
||||
result, &blob->certificate_length);
|
||||
if (error != ESP_OK) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (mbedtls_sha256(blob->certificate_der, blob->certificate_length,
|
||||
blob->certificate_fingerprint, 0) != 0) {
|
||||
error = ESP_FAIL;
|
||||
goto cleanup;
|
||||
}
|
||||
error = ESP_OK;
|
||||
|
||||
cleanup:
|
||||
secure_wipe(serial, sizeof(serial));
|
||||
mbedtls_x509write_crt_free(&writer);
|
||||
mbedtls_pk_free(&key);
|
||||
return error;
|
||||
}
|
||||
|
||||
static bool x509_time_equals(const mbedtls_x509_time *time,
|
||||
int year, int month, int day,
|
||||
int hour, int minute, int second)
|
||||
{
|
||||
return time->year == year && time->mon == month && time->day == day &&
|
||||
time->hour == hour && time->min == minute && time->sec == second;
|
||||
}
|
||||
|
||||
static bool name_is_single_common_name(const mbedtls_x509_name *name,
|
||||
const char *expected)
|
||||
{
|
||||
size_t expected_length = strlen(expected);
|
||||
return name != NULL && name->next == NULL &&
|
||||
name->oid.len == MBEDTLS_OID_SIZE(MBEDTLS_OID_AT_CN) &&
|
||||
memcmp(name->oid.p, MBEDTLS_OID_AT_CN, name->oid.len) == 0 &&
|
||||
name->val.len == expected_length &&
|
||||
memcmp(name->val.p, expected, expected_length) == 0;
|
||||
}
|
||||
|
||||
static bool extended_key_usage_is_server_auth_only(const mbedtls_x509_crt *certificate)
|
||||
{
|
||||
const mbedtls_x509_sequence *usage = &certificate->ext_key_usage;
|
||||
return usage->next == NULL &&
|
||||
usage->buf.p != NULL &&
|
||||
usage->buf.len == MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH) &&
|
||||
memcmp(usage->buf.p, MBEDTLS_OID_SERVER_AUTH, usage->buf.len) == 0;
|
||||
}
|
||||
|
||||
static bool subject_alt_names_are_expected(const mbedtls_x509_crt *certificate,
|
||||
const char *dns_name)
|
||||
{
|
||||
bool found_dns = false;
|
||||
bool found_ip = false;
|
||||
|
||||
for (const mbedtls_x509_sequence *item = &certificate->subject_alt_names;
|
||||
item != NULL && item->buf.p != NULL; item = item->next) {
|
||||
if (item->buf.tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC |
|
||||
MBEDTLS_X509_SAN_DNS_NAME) &&
|
||||
item->buf.len == strlen(dns_name) &&
|
||||
memcmp(item->buf.p, dns_name, item->buf.len) == 0 && !found_dns) {
|
||||
found_dns = true;
|
||||
} else if (item->buf.tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC |
|
||||
MBEDTLS_X509_SAN_IP_ADDRESS) &&
|
||||
item->buf.len == sizeof(s_ap_ipv4_address) &&
|
||||
memcmp(item->buf.p, s_ap_ipv4_address, item->buf.len) == 0 &&
|
||||
!found_ip) {
|
||||
found_ip = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return found_dns && found_ip;
|
||||
}
|
||||
|
||||
static bool certificate_self_signature_is_valid(mbedtls_x509_crt *certificate)
|
||||
{
|
||||
unsigned char *cursor = certificate->raw.p;
|
||||
const unsigned char *end = certificate->raw.p + certificate->raw.len;
|
||||
const unsigned char *outer_end;
|
||||
const unsigned char *signature;
|
||||
size_t outer_length = 0U;
|
||||
size_t field_length = 0U;
|
||||
size_t signature_length = 0U;
|
||||
uint8_t digest[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
bool valid = false;
|
||||
|
||||
/*
|
||||
* Mbed TLS exposes the TBS bytes but not the signature bytes. Parse only
|
||||
* the certificate's three outer fields instead of relying on private ABI.
|
||||
*/
|
||||
if (mbedtls_asn1_get_tag(&cursor, end, &outer_length,
|
||||
MBEDTLS_ASN1_CONSTRUCTED |
|
||||
MBEDTLS_ASN1_SEQUENCE) != 0 ||
|
||||
outer_length != (size_t)(end - cursor)) {
|
||||
goto cleanup;
|
||||
}
|
||||
outer_end = cursor + outer_length;
|
||||
if (mbedtls_asn1_get_tag(&cursor, outer_end, &field_length,
|
||||
MBEDTLS_ASN1_CONSTRUCTED |
|
||||
MBEDTLS_ASN1_SEQUENCE) != 0 ||
|
||||
field_length > (size_t)(outer_end - cursor)) {
|
||||
goto cleanup;
|
||||
}
|
||||
cursor += field_length;
|
||||
if (mbedtls_asn1_get_tag(&cursor, outer_end, &field_length,
|
||||
MBEDTLS_ASN1_CONSTRUCTED |
|
||||
MBEDTLS_ASN1_SEQUENCE) != 0 ||
|
||||
field_length > (size_t)(outer_end - cursor)) {
|
||||
goto cleanup;
|
||||
}
|
||||
cursor += field_length;
|
||||
if (mbedtls_asn1_get_bitstring_null(&cursor, outer_end,
|
||||
&signature_length) != 0 ||
|
||||
signature_length == 0U ||
|
||||
signature_length != (size_t)(outer_end - cursor)) {
|
||||
goto cleanup;
|
||||
}
|
||||
signature = cursor;
|
||||
|
||||
if (mbedtls_sha256(certificate->tbs.p, certificate->tbs.len,
|
||||
digest, 0) == 0 &&
|
||||
mbedtls_pk_verify(&certificate->pk, MBEDTLS_MD_SHA256,
|
||||
digest, sizeof(digest),
|
||||
signature, signature_length) == 0) {
|
||||
valid = true;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
secure_wipe(digest, sizeof(digest));
|
||||
return valid;
|
||||
}
|
||||
|
||||
static esp_err_t validate_certificate_and_key(const web_security_blob_t *blob)
|
||||
{
|
||||
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY] = {0};
|
||||
char dns_name[WEB_SECURITY_DNS_NAME_CAPACITY] = {0};
|
||||
uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
mbedtls_pk_context private_key;
|
||||
mbedtls_x509_crt certificate;
|
||||
esp_err_t error = ESP_ERR_INVALID_RESPONSE;
|
||||
|
||||
mbedtls_pk_init(&private_key);
|
||||
mbedtls_x509_crt_init(&certificate);
|
||||
|
||||
if (build_device_names(common_name, sizeof(common_name),
|
||||
dns_name, sizeof(dns_name)) != ESP_OK) {
|
||||
error = ESP_FAIL;
|
||||
goto cleanup;
|
||||
}
|
||||
if (mbedtls_sha256(blob->certificate_der, blob->certificate_length,
|
||||
fingerprint, 0) != 0 ||
|
||||
!constant_time_equal(fingerprint, blob->certificate_fingerprint,
|
||||
sizeof(fingerprint))) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (mbedtls_pk_parse_key(&private_key,
|
||||
blob->private_key_der, blob->private_key_length,
|
||||
NULL, 0U, secure_random_mbedtls, NULL) != 0 ||
|
||||
mbedtls_x509_crt_parse_der(&certificate,
|
||||
blob->certificate_der,
|
||||
blob->certificate_length) != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
mbedtls_ecp_keypair *private_ec = mbedtls_pk_ec(private_key);
|
||||
mbedtls_ecp_keypair *public_ec = mbedtls_pk_ec(certificate.pk);
|
||||
if (private_ec == NULL || public_ec == NULL ||
|
||||
mbedtls_ecp_keypair_get_group_id(private_ec) != MBEDTLS_ECP_DP_SECP256R1 ||
|
||||
mbedtls_ecp_keypair_get_group_id(public_ec) != MBEDTLS_ECP_DP_SECP256R1 ||
|
||||
mbedtls_pk_check_pair(&certificate.pk, &private_key,
|
||||
secure_random_mbedtls, NULL) != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (certificate.version != MBEDTLS_X509_CRT_VERSION_3 + 1 ||
|
||||
certificate.sig_oid.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_ECDSA_SHA256) ||
|
||||
memcmp(certificate.sig_oid.p, MBEDTLS_OID_ECDSA_SHA256,
|
||||
certificate.sig_oid.len) != 0 ||
|
||||
certificate.issuer_raw.len != certificate.subject_raw.len ||
|
||||
memcmp(certificate.issuer_raw.p, certificate.subject_raw.p,
|
||||
certificate.subject_raw.len) != 0 ||
|
||||
!name_is_single_common_name(&certificate.subject, common_name) ||
|
||||
!name_is_single_common_name(&certificate.issuer, common_name) ||
|
||||
!x509_time_equals(&certificate.valid_from, 2025, 1, 1, 0, 0, 0) ||
|
||||
!x509_time_equals(&certificate.valid_to, 2049, 12, 31, 23, 59, 59)) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!mbedtls_x509_crt_has_ext_type(&certificate,
|
||||
MBEDTLS_X509_EXT_BASIC_CONSTRAINTS) ||
|
||||
mbedtls_x509_crt_get_ca_istrue(&certificate) != 0 ||
|
||||
!mbedtls_x509_crt_has_ext_type(&certificate, MBEDTLS_X509_EXT_KEY_USAGE) ||
|
||||
mbedtls_x509_crt_check_key_usage(
|
||||
&certificate, MBEDTLS_X509_KU_DIGITAL_SIGNATURE) != 0 ||
|
||||
!mbedtls_x509_crt_has_ext_type(
|
||||
&certificate, MBEDTLS_X509_EXT_EXTENDED_KEY_USAGE) ||
|
||||
!extended_key_usage_is_server_auth_only(&certificate) ||
|
||||
mbedtls_x509_crt_check_extended_key_usage(
|
||||
&certificate, MBEDTLS_OID_SERVER_AUTH,
|
||||
MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH)) != 0 ||
|
||||
!mbedtls_x509_crt_has_ext_type(&certificate,
|
||||
MBEDTLS_X509_EXT_SUBJECT_ALT_NAME) ||
|
||||
!subject_alt_names_are_expected(&certificate, dns_name)) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!certificate_self_signature_is_valid(&certificate)) {
|
||||
goto cleanup;
|
||||
}
|
||||
error = ESP_OK;
|
||||
|
||||
cleanup:
|
||||
secure_wipe(fingerprint, sizeof(fingerprint));
|
||||
mbedtls_x509_crt_free(&certificate);
|
||||
mbedtls_pk_free(&private_key);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t validate_blob(const web_security_blob_t *blob)
|
||||
{
|
||||
if (blob == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (blob->schema_version != WEB_SECURITY_SCHEMA_VERSION ||
|
||||
blob->blob_size != WEB_SECURITY_BLOB_SIZE) {
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
if (blob->generation == 0U || blob->reserved_header != 0U ||
|
||||
blob->reserved_lengths != 0U ||
|
||||
!bytes_are_zero(blob->reserved, sizeof(blob->reserved)) ||
|
||||
blob->username_length != sizeof(s_admin_username) - 1U ||
|
||||
memcmp(blob->username, s_admin_username,
|
||||
sizeof(s_admin_username) - 1U) != 0 ||
|
||||
!unused_bytes_are_zero(blob->username, blob->username_length,
|
||||
sizeof(blob->username)) ||
|
||||
blob->password_length != WEB_SECURITY_PASSWORD_LENGTH ||
|
||||
!unused_bytes_are_zero(blob->password, blob->password_length,
|
||||
sizeof(blob->password)) ||
|
||||
blob->private_key_length == 0U ||
|
||||
blob->private_key_length > sizeof(blob->private_key_der) ||
|
||||
!unused_bytes_are_zero(blob->private_key_der, blob->private_key_length,
|
||||
sizeof(blob->private_key_der)) ||
|
||||
blob->certificate_length == 0U ||
|
||||
blob->certificate_length > sizeof(blob->certificate_der) ||
|
||||
!unused_bytes_are_zero(blob->certificate_der, blob->certificate_length,
|
||||
sizeof(blob->certificate_der))) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
for (size_t i = 0U; i < blob->password_length; ++i) {
|
||||
const uint8_t value = blob->password[i];
|
||||
bool valid = (value >= 'A' && value <= 'Z') ||
|
||||
(value >= 'a' && value <= 'z') ||
|
||||
(value >= '0' && value <= '9') ||
|
||||
value == '-' || value == '_';
|
||||
if (!valid) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
}
|
||||
return validate_certificate_and_key(blob);
|
||||
}
|
||||
|
||||
static esp_err_t generate_all(web_security_blob_t *blob, uint32_t generation)
|
||||
{
|
||||
memset(blob, 0, sizeof(*blob));
|
||||
blob->schema_version = WEB_SECURITY_SCHEMA_VERSION;
|
||||
blob->blob_size = WEB_SECURITY_BLOB_SIZE;
|
||||
blob->generation = generation;
|
||||
|
||||
esp_err_t error = generate_credentials(blob);
|
||||
if (error == ESP_OK) {
|
||||
error = generate_certificate(blob);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = validate_blob(blob);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t save_blob(const web_security_blob_t *blob)
|
||||
{
|
||||
esp_err_t error = validate_blob(blob);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
nvs_handle_t handle;
|
||||
error = nvs_open(WEB_SECURITY_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
/* NVS append semantics retain the committed predecessor until commit succeeds. */
|
||||
error = nvs_set_blob(handle, WEB_SECURITY_NVS_BLOB_KEY,
|
||||
blob, sizeof(*blob));
|
||||
if (error == ESP_OK) {
|
||||
error = nvs_commit(handle);
|
||||
}
|
||||
nvs_close(handle);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing)
|
||||
{
|
||||
*missing = false;
|
||||
nvs_handle_t handle;
|
||||
esp_err_t error = nvs_open(WEB_SECURITY_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||
if (error == ESP_ERR_NVS_NOT_FOUND) {
|
||||
*missing = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
size_t size = 0U;
|
||||
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, NULL, &size);
|
||||
if (error == ESP_ERR_NVS_NOT_FOUND) {
|
||||
*missing = true;
|
||||
nvs_close(handle);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (error == ESP_ERR_NVS_TYPE_MISMATCH) {
|
||||
nvs_close(handle);
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
nvs_close(handle);
|
||||
return error;
|
||||
}
|
||||
if (size != sizeof(*blob)) {
|
||||
nvs_close(handle);
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
|
||||
memset(blob, 0, sizeof(*blob));
|
||||
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, blob, &size);
|
||||
nvs_close(handle);
|
||||
if (error == ESP_ERR_NVS_INVALID_LENGTH) {
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
return validate_blob(blob);
|
||||
}
|
||||
|
||||
esp_err_t web_security_init(web_security_load_result_t *load_result)
|
||||
{
|
||||
esp_err_t error = secure_random_init();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
error = ensure_security_mutex();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
if (s_material_ready) {
|
||||
if (load_result != NULL) {
|
||||
*load_result = s_load_result;
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
web_security_blob_t candidate;
|
||||
bool missing = false;
|
||||
error = load_stored_blob(&candidate, &missing);
|
||||
if (error == ESP_OK && missing) {
|
||||
error = generate_all(&candidate, 1U);
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
s_material = candidate;
|
||||
s_material_ready = true;
|
||||
s_load_result = missing ? WEB_SECURITY_LOAD_GENERATED_MISSING
|
||||
: WEB_SECURITY_LOAD_STORED;
|
||||
if (load_result != NULL) {
|
||||
*load_result = s_load_result;
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_security_copy_tls_material(
|
||||
uint8_t *certificate, size_t certificate_capacity,
|
||||
size_t *certificate_length,
|
||||
uint8_t *private_key, size_t private_key_capacity,
|
||||
size_t *private_key_length)
|
||||
{
|
||||
if (certificate_length == NULL || private_key_length == NULL ||
|
||||
(certificate == NULL && certificate_capacity != 0U) ||
|
||||
(private_key == NULL && private_key_capacity != 0U)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
*certificate_length = s_material.certificate_length;
|
||||
*private_key_length = s_material.private_key_length;
|
||||
if ((certificate == NULL && certificate_capacity != 0U) ||
|
||||
(private_key == NULL && private_key_capacity != 0U)) {
|
||||
error = ESP_ERR_INVALID_ARG;
|
||||
} else if ((certificate != NULL &&
|
||||
certificate_capacity < s_material.certificate_length) ||
|
||||
(private_key != NULL &&
|
||||
private_key_capacity < s_material.private_key_length)) {
|
||||
/* Check both capacities before copying either half of the pair. */
|
||||
error = ESP_ERR_INVALID_SIZE;
|
||||
} else {
|
||||
if (certificate != NULL) {
|
||||
memcpy(certificate, s_material.certificate_der,
|
||||
s_material.certificate_length);
|
||||
}
|
||||
if (private_key != NULL) {
|
||||
memcpy(private_key, s_material.private_key_der,
|
||||
s_material.private_key_length);
|
||||
}
|
||||
error = ESP_OK;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_security_copy_username(char *output, size_t capacity,
|
||||
size_t *output_length)
|
||||
{
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
if (output_length == NULL || (output == NULL && capacity != 0U)) {
|
||||
error = ESP_ERR_INVALID_ARG;
|
||||
} else {
|
||||
*output_length = s_material.username_length;
|
||||
if (output == NULL) {
|
||||
error = capacity == 0U ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||
} else if (capacity <= s_material.username_length) {
|
||||
error = ESP_ERR_INVALID_SIZE;
|
||||
} else {
|
||||
memcpy(output, s_material.username, s_material.username_length);
|
||||
output[s_material.username_length] = '\0';
|
||||
error = ESP_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t credential_digest(const uint8_t *username, size_t username_length,
|
||||
const uint8_t *password, size_t password_length,
|
||||
uint8_t digest[WEB_SECURITY_SHA256_LENGTH])
|
||||
{
|
||||
uint8_t canonical[4U + WEB_SECURITY_USERNAME_CAPACITY +
|
||||
WEB_SECURITY_PASSWORD_CAPACITY] = {0};
|
||||
size_t offset = 0U;
|
||||
|
||||
canonical[offset++] = (uint8_t)(username_length >> 8U);
|
||||
canonical[offset++] = (uint8_t)username_length;
|
||||
if (username_length > 0U) {
|
||||
memcpy(canonical + offset, username, username_length);
|
||||
offset += username_length;
|
||||
}
|
||||
canonical[offset++] = (uint8_t)(password_length >> 8U);
|
||||
canonical[offset++] = (uint8_t)password_length;
|
||||
if (password_length > 0U) {
|
||||
memcpy(canonical + offset, password, password_length);
|
||||
offset += password_length;
|
||||
}
|
||||
|
||||
int result = mbedtls_sha256(canonical, offset, digest, 0);
|
||||
secure_wipe(canonical, sizeof(canonical));
|
||||
return result == 0 ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t web_security_authenticate_basic(const uint8_t *username,
|
||||
size_t username_length,
|
||||
const uint8_t *password,
|
||||
size_t password_length,
|
||||
bool *authenticated)
|
||||
{
|
||||
if (authenticated == NULL ||
|
||||
(username == NULL && username_length != 0U) ||
|
||||
(password == NULL && password_length != 0U)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*authenticated = false;
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (username_length > WEB_SECURITY_USERNAME_CAPACITY ||
|
||||
password_length > WEB_SECURITY_PASSWORD_CAPACITY) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t supplied_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
uint8_t expected_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
error = credential_digest(username, username_length,
|
||||
password, password_length,
|
||||
supplied_digest);
|
||||
if (error == ESP_OK) {
|
||||
error = credential_digest(s_material.username,
|
||||
s_material.username_length,
|
||||
s_material.password,
|
||||
s_material.password_length,
|
||||
expected_digest);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
*authenticated = constant_time_equal(supplied_digest,
|
||||
expected_digest,
|
||||
sizeof(expected_digest));
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
secure_wipe(supplied_digest, sizeof(supplied_digest));
|
||||
secure_wipe(expected_digest, sizeof(expected_digest));
|
||||
return error;
|
||||
}
|
||||
|
||||
static void copy_credentials_locked(web_security_credentials_t *credentials,
|
||||
const web_security_blob_t *blob)
|
||||
{
|
||||
memset(credentials, 0, sizeof(*credentials));
|
||||
credentials->username_length = blob->username_length;
|
||||
credentials->password_length = blob->password_length;
|
||||
memcpy(credentials->username, blob->username, blob->username_length);
|
||||
memcpy(credentials->password, blob->password, blob->password_length);
|
||||
}
|
||||
|
||||
esp_err_t web_security_show_credentials(web_security_credentials_t *credentials)
|
||||
{
|
||||
if (credentials == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
copy_credentials_locked(credentials, &s_material);
|
||||
error = ESP_OK;
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_security_get_certificate_metadata(
|
||||
web_security_certificate_metadata_t *metadata)
|
||||
{
|
||||
if (metadata == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
memset(metadata, 0, sizeof(*metadata));
|
||||
error = build_device_names(metadata->common_name,
|
||||
sizeof(metadata->common_name),
|
||||
metadata->dns_name,
|
||||
sizeof(metadata->dns_name));
|
||||
if (error == ESP_OK) {
|
||||
metadata->material_generation = s_material.generation;
|
||||
memcpy(metadata->sha256_fingerprint,
|
||||
s_material.certificate_fingerprint,
|
||||
sizeof(metadata->sha256_fingerprint));
|
||||
memcpy(metadata->ipv4_address, s_ap_ipv4_address,
|
||||
sizeof(metadata->ipv4_address));
|
||||
memcpy(metadata->not_before, WEB_SECURITY_CERT_NOT_BEFORE,
|
||||
sizeof(WEB_SECURITY_CERT_NOT_BEFORE));
|
||||
memcpy(metadata->not_after, WEB_SECURITY_CERT_NOT_AFTER,
|
||||
sizeof(WEB_SECURITY_CERT_NOT_AFTER));
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t increment_generation(web_security_blob_t *blob)
|
||||
{
|
||||
if (blob->generation == UINT32_MAX) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
++blob->generation;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void install_committed_blob(const web_security_blob_t *candidate)
|
||||
{
|
||||
/* Wipe the superseded private key before replacing the live snapshot. */
|
||||
secure_wipe(&s_material, sizeof(s_material));
|
||||
s_material = *candidate;
|
||||
s_material_ready = true;
|
||||
s_load_result = WEB_SECURITY_LOAD_STORED;
|
||||
}
|
||||
|
||||
esp_err_t web_security_rotate_credentials(web_security_credentials_t *new_credentials)
|
||||
{
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
web_security_blob_t candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
if (s_material_ready) {
|
||||
candidate = s_material;
|
||||
error = increment_generation(&candidate);
|
||||
if (error == ESP_OK) {
|
||||
error = generate_credentials(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_committed_blob(&candidate);
|
||||
if (new_credentials != NULL) {
|
||||
copy_credentials_locked(new_credentials, &s_material);
|
||||
}
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_security_rotate_certificate(void)
|
||||
{
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
web_security_blob_t candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
if (s_material_ready) {
|
||||
candidate = s_material;
|
||||
error = increment_generation(&candidate);
|
||||
if (error == ESP_OK) {
|
||||
error = generate_certificate(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_committed_blob(&candidate);
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_security_reset_all(web_security_credentials_t *new_credentials)
|
||||
{
|
||||
esp_err_t error = secure_random_init();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
error = ensure_security_mutex();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
web_security_blob_t candidate;
|
||||
uint32_t generation = 1U;
|
||||
if (s_material_ready) {
|
||||
if (s_material.generation == UINT32_MAX) {
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
generation = s_material.generation + 1U;
|
||||
}
|
||||
|
||||
error = generate_all(&candidate, generation);
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_committed_blob(&candidate);
|
||||
if (new_credentials != NULL) {
|
||||
copy_credentials_locked(new_credentials, &s_material);
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Persistent HTTPS identity and administrative Basic credentials. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define WEB_SECURITY_NVS_NAMESPACE "web_sec"
|
||||
#define WEB_SECURITY_NVS_BLOB_KEY "material"
|
||||
|
||||
#define WEB_SECURITY_USERNAME_CAPACITY 16U
|
||||
#define WEB_SECURITY_PASSWORD_CAPACITY 32U
|
||||
#define WEB_SECURITY_PASSWORD_LENGTH 24U
|
||||
#define WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY 256U
|
||||
#define WEB_SECURITY_CERTIFICATE_DER_CAPACITY 1024U
|
||||
#define WEB_SECURITY_SHA256_LENGTH 32U
|
||||
#define WEB_SECURITY_COMMON_NAME_CAPACITY 32U
|
||||
#define WEB_SECURITY_DNS_NAME_CAPACITY 40U
|
||||
|
||||
#define WEB_SECURITY_CERT_NOT_BEFORE "20250101000000"
|
||||
#define WEB_SECURITY_CERT_NOT_AFTER "20491231235959"
|
||||
|
||||
typedef enum {
|
||||
WEB_SECURITY_LOAD_STORED = 0,
|
||||
WEB_SECURITY_LOAD_GENERATED_MISSING = 1,
|
||||
} web_security_load_result_t;
|
||||
|
||||
/*
|
||||
* This intentionally contains a displayable secret. UART callers should call
|
||||
* secure_wipe() on it immediately after rendering the length-delimited fields.
|
||||
*/
|
||||
typedef struct {
|
||||
size_t username_length;
|
||||
size_t password_length;
|
||||
char username[WEB_SECURITY_USERNAME_CAPACITY + 1U];
|
||||
char password[WEB_SECURITY_PASSWORD_CAPACITY + 1U];
|
||||
} web_security_credentials_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t material_generation;
|
||||
uint8_t sha256_fingerprint[WEB_SECURITY_SHA256_LENGTH];
|
||||
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY];
|
||||
char dns_name[WEB_SECURITY_DNS_NAME_CAPACITY];
|
||||
uint8_t ipv4_address[4];
|
||||
char not_before[sizeof(WEB_SECURITY_CERT_NOT_BEFORE)];
|
||||
char not_after[sizeof(WEB_SECURITY_CERT_NOT_AFTER)];
|
||||
} web_security_certificate_metadata_t;
|
||||
|
||||
/*
|
||||
* NVS must already be initialized. Missing material is generated and saved;
|
||||
* an existing wrong-version blob returns ESP_ERR_INVALID_VERSION, while any
|
||||
* malformed or cryptographically inconsistent blob returns
|
||||
* ESP_ERR_INVALID_RESPONSE and is never overwritten. Call before radio startup
|
||||
* so secure_random_init() can seed from the pre-radio entropy source.
|
||||
*/
|
||||
esp_err_t web_security_init(web_security_load_result_t *load_result);
|
||||
|
||||
/*
|
||||
* Query with both outputs NULL/capacities zero. Certificate and key are copied
|
||||
* under one lock so a concurrent rotation can never produce a mismatched pair.
|
||||
*/
|
||||
esp_err_t web_security_copy_tls_material(
|
||||
uint8_t *certificate, size_t certificate_capacity,
|
||||
size_t *certificate_length,
|
||||
uint8_t *private_key, size_t private_key_capacity,
|
||||
size_t *private_key_length);
|
||||
esp_err_t web_security_copy_username(char *output, size_t capacity,
|
||||
size_t *output_length);
|
||||
|
||||
/* Input fields are decoded HTTP Basic components, not the base64 header text. */
|
||||
esp_err_t web_security_authenticate_basic(const uint8_t *username,
|
||||
size_t username_length,
|
||||
const uint8_t *password,
|
||||
size_t password_length,
|
||||
bool *authenticated);
|
||||
|
||||
/* Explicit secret-bearing API intended for a physically attached UART CLI. */
|
||||
esp_err_t web_security_show_credentials(web_security_credentials_t *credentials);
|
||||
esp_err_t web_security_get_certificate_metadata(
|
||||
web_security_certificate_metadata_t *metadata);
|
||||
|
||||
/* Mutations become visible only after a complete blob has committed to NVS. */
|
||||
esp_err_t web_security_rotate_credentials(web_security_credentials_t *new_credentials);
|
||||
esp_err_t web_security_rotate_certificate(void);
|
||||
|
||||
/* Explicitly replaces missing, valid, or incompatible stored material. */
|
||||
esp_err_t web_security_reset_all(web_security_credentials_t *new_credentials);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,482 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* TLS-only HTTP server with bounded Basic authentication and status output. */
|
||||
|
||||
#include "web_server.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_https_server.h"
|
||||
#include "esp_netif_ip_addr.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "secure_random.h"
|
||||
#include "serial_config.h"
|
||||
#include "serial_service.h"
|
||||
#include "session_broker.h"
|
||||
#include "usb_cdc_transport.h"
|
||||
#include "web_security.h"
|
||||
#include "wifi_manager.h"
|
||||
|
||||
#define WEB_SERVER_PORT 443U
|
||||
#define WEB_SERVER_MAX_AUTHORIZATION 128U
|
||||
#define WEB_SERVER_MAX_BASIC_DECODED 64U
|
||||
#define WEB_SERVER_STATUS_JSON_CAPACITY 2304U
|
||||
|
||||
static const char s_index_html[] =
|
||||
"<!doctype html><html lang=en><meta charset=utf-8>"
|
||||
"<meta name=viewport content=\"width=device-width,initial-scale=1\">"
|
||||
"<title>ESP32 Serial Swiss Army Knife</title>"
|
||||
"<style>body{font:16px system-ui;max-width:54rem;margin:3rem auto;padding:0 1rem}"
|
||||
"pre{background:#171717;color:#eee;padding:1rem;overflow:auto}</style>"
|
||||
"<h1>ESP32 Serial Swiss Army Knife</h1>"
|
||||
"<p>Authenticated HTTPS is operational. Interactive web serial access is not enabled yet.</p>"
|
||||
"<p><a href=/api/status>JSON status</a></p></html>";
|
||||
|
||||
static SemaphoreHandle_t s_server_mutex;
|
||||
static httpd_handle_t s_server;
|
||||
static bool s_initialized;
|
||||
static bool s_transitioning;
|
||||
static esp_err_t s_last_error = ESP_ERR_INVALID_STATE;
|
||||
static web_server_counters_t s_counters;
|
||||
|
||||
static esp_err_t ensure_mutex(void)
|
||||
{
|
||||
if (s_server_mutex != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
s_server_mutex = xSemaphoreCreateMutex();
|
||||
return s_server_mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
static void increment_counter(uint64_t *counter)
|
||||
{
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
++*counter;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
}
|
||||
|
||||
static esp_err_t set_common_headers(httpd_req_t *request)
|
||||
{
|
||||
esp_err_t error = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_set_hdr(request, "Referrer-Policy", "no-referrer");
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t send_plain_error(httpd_req_t *request,
|
||||
const char *status,
|
||||
const char *message)
|
||||
{
|
||||
esp_err_t error = httpd_resp_set_status(request, status);
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_set_type(request, "text/plain; charset=utf-8");
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = set_common_headers(request);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_sendstr(request, message);
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
increment_counter(&s_counters.response_errors);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t send_authentication_required(httpd_req_t *request)
|
||||
{
|
||||
esp_err_t error = httpd_resp_set_hdr(
|
||||
request, "WWW-Authenticate",
|
||||
"Basic realm=\"ESP32-SAK\", charset=\"UTF-8\"");
|
||||
if (error != ESP_OK) {
|
||||
increment_counter(&s_counters.response_errors);
|
||||
return error;
|
||||
}
|
||||
return send_plain_error(request, "401 Unauthorized", "Authentication required.\n");
|
||||
}
|
||||
|
||||
static esp_err_t authenticate_request(httpd_req_t *request, bool *authenticated)
|
||||
{
|
||||
char authorization[WEB_SERVER_MAX_AUTHORIZATION] = {0};
|
||||
uint8_t decoded[WEB_SERVER_MAX_BASIC_DECODED] = {0};
|
||||
size_t decoded_length = 0U;
|
||||
esp_err_t result = ESP_OK;
|
||||
*authenticated = false;
|
||||
|
||||
increment_counter(&s_counters.requests);
|
||||
size_t header_length = httpd_req_get_hdr_value_len(request, "Authorization");
|
||||
if (header_length == 0U || header_length >= sizeof(authorization)) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (httpd_req_get_hdr_value_str(request, "Authorization",
|
||||
authorization, sizeof(authorization)) != ESP_OK ||
|
||||
header_length < 7U || strncasecmp(authorization, "Basic ", 6U) != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
int decode_result = mbedtls_base64_decode(
|
||||
decoded, sizeof(decoded), &decoded_length,
|
||||
(const unsigned char *)authorization + 6U, header_length - 6U);
|
||||
if (decode_result != 0 || decoded_length == 0U) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
uint8_t *separator = memchr(decoded, ':', decoded_length);
|
||||
if (separator == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
size_t username_length = (size_t)(separator - decoded);
|
||||
size_t password_length = decoded_length - username_length - 1U;
|
||||
result = web_security_authenticate_basic(decoded, username_length,
|
||||
separator + 1U, password_length,
|
||||
authenticated);
|
||||
|
||||
cleanup:
|
||||
secure_wipe(authorization, sizeof(authorization));
|
||||
secure_wipe(decoded, sizeof(decoded));
|
||||
if (result != ESP_OK) {
|
||||
return result;
|
||||
}
|
||||
if (*authenticated) {
|
||||
increment_counter(&s_counters.authenticated_requests);
|
||||
} else {
|
||||
increment_counter(&s_counters.authentication_failures);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t authorize_or_respond(httpd_req_t *request, bool *authorized)
|
||||
{
|
||||
*authorized = false;
|
||||
esp_err_t error = authenticate_request(request, authorized);
|
||||
if (error != ESP_OK) {
|
||||
*authorized = false;
|
||||
return send_plain_error(request, "503 Service Unavailable",
|
||||
"Authentication service unavailable.\n");
|
||||
}
|
||||
return *authorized ? ESP_OK : send_authentication_required(request);
|
||||
}
|
||||
|
||||
static esp_err_t root_handler(httpd_req_t *request)
|
||||
{
|
||||
bool authorized = false;
|
||||
esp_err_t error = authorize_or_respond(request, &authorized);
|
||||
if (error != ESP_OK || !authorized) {
|
||||
return error;
|
||||
}
|
||||
increment_counter(&s_counters.root_requests);
|
||||
|
||||
error = httpd_resp_set_type(request, "text/html; charset=utf-8");
|
||||
if (error == ESP_OK) {
|
||||
error = set_common_headers(request);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_set_hdr(
|
||||
request, "Content-Security-Policy",
|
||||
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'");
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_send(request, s_index_html, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
increment_counter(&s_counters.response_errors);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static const char *safe_string(const char *value)
|
||||
{
|
||||
return value != NULL ? value : "unknown";
|
||||
}
|
||||
|
||||
static void format_ipv4(uint32_t address, char output[16])
|
||||
{
|
||||
if (address == 0U) {
|
||||
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
|
||||
return;
|
||||
}
|
||||
esp_ip4_addr_t ip = {.addr = address};
|
||||
int written = snprintf(output, 16U, IPSTR, IP2STR(&ip));
|
||||
if (written < 0 || written >= 16) {
|
||||
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
|
||||
}
|
||||
}
|
||||
|
||||
static void format_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH],
|
||||
char output[(WEB_SECURITY_SHA256_LENGTH * 3U)])
|
||||
{
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
size_t offset = 0U;
|
||||
for (size_t index = 0U; index < WEB_SECURITY_SHA256_LENGTH; ++index) {
|
||||
if (index != 0U) {
|
||||
output[offset++] = ':';
|
||||
}
|
||||
output[offset++] = hex[fingerprint[index] >> 4U];
|
||||
output[offset++] = hex[fingerprint[index] & 0x0fU];
|
||||
}
|
||||
output[offset] = '\0';
|
||||
}
|
||||
|
||||
static esp_err_t status_handler(httpd_req_t *request)
|
||||
{
|
||||
bool authorized = false;
|
||||
esp_err_t error = authorize_or_respond(request, &authorized);
|
||||
if (error != ESP_OK || !authorized) {
|
||||
return error;
|
||||
}
|
||||
increment_counter(&s_counters.status_requests);
|
||||
|
||||
wifi_manager_snapshot_t wifi = {0};
|
||||
serial_config_t serial_config = {0};
|
||||
serial_service_counters_t serial_counters = {0};
|
||||
session_broker_global_snapshot_t broker = {0};
|
||||
usb_cdc_transport_snapshot_t usb = {0};
|
||||
web_server_snapshot_t web = {0};
|
||||
web_security_certificate_metadata_t certificate = {0};
|
||||
char ipv4[16] = {0};
|
||||
char fingerprint[WEB_SECURITY_SHA256_LENGTH * 3U] = {0};
|
||||
char response[WEB_SERVER_STATUS_JSON_CAPACITY];
|
||||
|
||||
bool wifi_available = wifi_manager_get_snapshot(&wifi) == ESP_OK;
|
||||
bool serial_config_available = serial_service_get_config(&serial_config) == ESP_OK;
|
||||
serial_service_get_counters(&serial_counters);
|
||||
bool broker_available = session_broker_get_global_snapshot(&broker) == ESP_OK;
|
||||
bool usb_available = usb_cdc_transport_get_snapshot(&usb) == ESP_OK;
|
||||
bool web_available = web_server_get_snapshot(&web) == ESP_OK;
|
||||
bool certificate_available =
|
||||
web_security_get_certificate_metadata(&certificate) == ESP_OK;
|
||||
|
||||
format_ipv4(wifi_available ? wifi.ip : 0U, ipv4);
|
||||
if (certificate_available) {
|
||||
format_fingerprint(certificate.sha256_fingerprint, fingerprint);
|
||||
} else {
|
||||
memcpy(fingerprint, "unavailable", sizeof("unavailable"));
|
||||
}
|
||||
|
||||
int written = snprintf(
|
||||
response, sizeof(response),
|
||||
"{\n"
|
||||
" \"uptime_ms\":%" PRIu64 ",\n"
|
||||
" \"wifi\":{\"available\":%s,\"state\":\"%s\",\"sta_ipv4\":\"%s\","
|
||||
"\"rssi\":%d,\"channel\":%u,\"ap_running\":%s,\"ap_clients\":%u},\n"
|
||||
" \"serial\":{\"running\":%s,\"config_available\":%s,\"baud\":%" PRIu32 ","
|
||||
"\"data_bits\":\"%s\",\"parity\":\"%s\",\"stop_bits\":\"%s\","
|
||||
"\"flow\":\"%s\",\"rx_bytes\":%" PRIu64 ",\"rx_dropped\":%" PRIu64 ","
|
||||
"\"tx_sent\":%" PRIu64 ",\"tx_dropped\":%" PRIu64 "},\n"
|
||||
" \"broker\":{\"available\":%s,\"clients\":%" PRIu32 ",\"writer\":%" PRIu32 ","
|
||||
"\"uart_rx_bytes\":%" PRIu64 ",\"observer_dropped\":%" PRIu64 "},\n"
|
||||
" \"usb\":{\"available\":%s,\"attached\":%s,\"host_open\":%s,\"writer\":%s},\n"
|
||||
" \"https\":{\"running\":%s,\"requests\":%" PRIu64 ","
|
||||
"\"authenticated_requests\":%" PRIu64 ",\"authentication_failures\":%" PRIu64 ","
|
||||
"\"certificate_sha256\":\"%s\"}\n"
|
||||
"}\n",
|
||||
(uint64_t)(esp_timer_get_time() / 1000),
|
||||
wifi_available ? "true" : "false",
|
||||
wifi_available ? wifi_manager_state_to_string(wifi.state) : "unavailable",
|
||||
ipv4, wifi_available ? (int)wifi.sta_rssi : 0,
|
||||
wifi_available ? (unsigned int)wifi.sta_channel : 0U,
|
||||
wifi_available && wifi.ap_running ? "true" : "false",
|
||||
wifi_available ? (unsigned int)wifi.ap_client_count : 0U,
|
||||
serial_service_is_running() ? "true" : "false",
|
||||
serial_config_available ? "true" : "false",
|
||||
serial_config_available ? serial_config.baud_rate : 0U,
|
||||
serial_config_available ? safe_string(serial_config_data_bits_to_string(serial_config.data_bits)) : "unknown",
|
||||
serial_config_available ? safe_string(serial_config_parity_to_string(serial_config.parity)) : "unknown",
|
||||
serial_config_available ? safe_string(serial_config_stop_bits_to_string(serial_config.stop_bits)) : "unknown",
|
||||
serial_config_available ? safe_string(serial_config_flow_control_to_string(serial_config.flow_control)) : "unknown",
|
||||
serial_counters.rx_bytes, serial_counters.rx_dropped_bytes,
|
||||
serial_counters.tx_sent_to_uart_bytes, serial_counters.tx_dropped_bytes,
|
||||
broker_available ? "true" : "false",
|
||||
broker_available ? broker.connected_clients : 0U,
|
||||
broker_available ? broker.writer_id : SESSION_BROKER_NO_CLIENT,
|
||||
broker_available ? broker.counters.uart_rx_bytes : 0U,
|
||||
broker_available ? broker.counters.output_dropped_bytes : 0U,
|
||||
usb_available ? "true" : "false",
|
||||
usb_available && usb.attached ? "true" : "false",
|
||||
usb_available && usb.attached && usb.dtr ? "true" : "false",
|
||||
usb_available && usb.writer ? "true" : "false",
|
||||
web_available && web.running ? "true" : "false",
|
||||
web_available ? web.counters.requests : 0U,
|
||||
web_available ? web.counters.authenticated_requests : 0U,
|
||||
web_available ? web.counters.authentication_failures : 0U,
|
||||
fingerprint);
|
||||
|
||||
if (written < 0 || (size_t)written >= sizeof(response)) {
|
||||
return send_plain_error(request, "500 Internal Server Error",
|
||||
"Status response overflow.\n");
|
||||
}
|
||||
error = httpd_resp_set_type(request, "application/json; charset=utf-8");
|
||||
if (error == ESP_OK) {
|
||||
error = set_common_headers(request);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_send(request, response, (ssize_t)written);
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
increment_counter(&s_counters.response_errors);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static const httpd_uri_t s_root_uri = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = root_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_status_uri = {
|
||||
.uri = "/api/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = status_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
|
||||
esp_err_t web_server_init(void)
|
||||
{
|
||||
esp_err_t error = ensure_mutex();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_initialized = true;
|
||||
if (s_last_error == ESP_ERR_INVALID_STATE) {
|
||||
s_last_error = ESP_OK;
|
||||
}
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_start(void)
|
||||
{
|
||||
esp_err_t error = web_server_init();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
if (s_server != NULL || s_transitioning) {
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
s_transitioning = true;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
uint8_t certificate[WEB_SECURITY_CERTIFICATE_DER_CAPACITY] = {0};
|
||||
uint8_t private_key[WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY] = {0};
|
||||
size_t certificate_length = 0U;
|
||||
size_t private_key_length = 0U;
|
||||
httpd_handle_t server = NULL;
|
||||
|
||||
error = web_security_copy_tls_material(
|
||||
certificate, sizeof(certificate), &certificate_length,
|
||||
private_key, sizeof(private_key), &private_key_length);
|
||||
if (error == ESP_OK) {
|
||||
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
|
||||
config.httpd.max_open_sockets = 2;
|
||||
config.httpd.max_uri_handlers = 2;
|
||||
config.httpd.lru_purge_enable = true;
|
||||
config.servercert = certificate;
|
||||
config.servercert_len = certificate_length;
|
||||
config.prvtkey_pem = private_key;
|
||||
config.prvtkey_len = private_key_length;
|
||||
config.port_secure = WEB_SERVER_PORT;
|
||||
config.tls_handshake_timeout_ms = 5000U;
|
||||
error = httpd_ssl_start(&server, &config);
|
||||
}
|
||||
secure_wipe(certificate, sizeof(certificate));
|
||||
secure_wipe(private_key, sizeof(private_key));
|
||||
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_register_uri_handler(server, &s_root_uri);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_register_uri_handler(server, &s_status_uri);
|
||||
}
|
||||
if (error != ESP_OK && server != NULL) {
|
||||
(void)httpd_ssl_stop(server);
|
||||
server = NULL;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = error;
|
||||
if (error == ESP_OK) {
|
||||
s_server = server;
|
||||
++s_counters.starts;
|
||||
} else {
|
||||
++s_counters.start_failures;
|
||||
}
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_server_stop(void)
|
||||
{
|
||||
if (s_server_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
if (s_server == NULL || s_transitioning) {
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
httpd_handle_t server = s_server;
|
||||
s_transitioning = true;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
esp_err_t error = httpd_ssl_stop(server);
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = error;
|
||||
if (error == ESP_OK) {
|
||||
s_server = NULL;
|
||||
++s_counters.stops;
|
||||
}
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot)
|
||||
{
|
||||
if (snapshot == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_server_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
memset(snapshot, 0, sizeof(*snapshot));
|
||||
snapshot->initialized = s_initialized;
|
||||
snapshot->running = s_server != NULL;
|
||||
snapshot->transitioning = s_transitioning;
|
||||
snapshot->port = WEB_SERVER_PORT;
|
||||
snapshot->last_error = s_last_error;
|
||||
snapshot->counters = s_counters;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_clear_counters(void)
|
||||
{
|
||||
if (s_server_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
memset(&s_counters, 0, sizeof(s_counters));
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Authenticated HTTPS administration foundation. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint64_t starts;
|
||||
uint64_t start_failures;
|
||||
uint64_t stops;
|
||||
uint64_t requests;
|
||||
uint64_t authenticated_requests;
|
||||
uint64_t authentication_failures;
|
||||
uint64_t root_requests;
|
||||
uint64_t status_requests;
|
||||
uint64_t response_errors;
|
||||
} web_server_counters_t;
|
||||
|
||||
typedef struct {
|
||||
bool initialized;
|
||||
bool running;
|
||||
bool transitioning;
|
||||
uint16_t port;
|
||||
esp_err_t last_error;
|
||||
web_server_counters_t counters;
|
||||
} web_server_snapshot_t;
|
||||
|
||||
/* Initialize runtime state without requiring valid certificate material. */
|
||||
esp_err_t web_server_init(void);
|
||||
|
||||
/* Start one TLS-only server on all active network interfaces. */
|
||||
esp_err_t web_server_start(void);
|
||||
esp_err_t web_server_stop(void);
|
||||
|
||||
esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot);
|
||||
esp_err_t web_server_clear_counters(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+4
-75
@@ -3,18 +3,13 @@
|
||||
|
||||
#include "wifi_config.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bootloader_random.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "mbedtls/ctr_drbg.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "secure_random.h"
|
||||
|
||||
_Static_assert(sizeof(wifi_config_sta_profile_t) == WIFI_CONFIG_STA_PROFILE_BLOB_SIZE,
|
||||
"Wi-Fi STA profile schema size changed");
|
||||
@@ -23,67 +18,7 @@ _Static_assert(offsetof(wifi_app_config_t, profiles) == 112U,
|
||||
_Static_assert(sizeof(wifi_app_config_t) == WIFI_CONFIG_BLOB_SIZE,
|
||||
"Wi-Fi config schema size changed");
|
||||
|
||||
static SemaphoreHandle_t s_drbg_mutex;
|
||||
static mbedtls_ctr_drbg_context s_drbg;
|
||||
static bool s_drbg_ready;
|
||||
|
||||
static int early_entropy_source(void *context, unsigned char *output, size_t length)
|
||||
{
|
||||
(void)context;
|
||||
/* This callback is used only during the explicit pre-radio initialization. */
|
||||
bootloader_random_enable();
|
||||
esp_fill_random(output, length);
|
||||
bootloader_random_disable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t wifi_config_entropy_init(void)
|
||||
{
|
||||
static const unsigned char personalization[] = "esp32-serial-sak-wifi";
|
||||
|
||||
if (s_drbg_ready) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (s_drbg_mutex == NULL) {
|
||||
s_drbg_mutex = xSemaphoreCreateMutex();
|
||||
if (s_drbg_mutex == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_OK;
|
||||
if (!s_drbg_ready) {
|
||||
mbedtls_ctr_drbg_init(&s_drbg);
|
||||
int result = mbedtls_ctr_drbg_seed(&s_drbg,
|
||||
early_entropy_source,
|
||||
NULL,
|
||||
personalization,
|
||||
sizeof(personalization) - 1U);
|
||||
if (result == 0) {
|
||||
/* Credential generation is rare; never invoke early entropy after RF starts. */
|
||||
mbedtls_ctr_drbg_set_reseed_interval(&s_drbg, INT_MAX);
|
||||
s_drbg_ready = true;
|
||||
} else {
|
||||
mbedtls_ctr_drbg_free(&s_drbg);
|
||||
error = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_drbg_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t fill_credential_random(uint8_t *output, size_t length)
|
||||
{
|
||||
if (!s_drbg_ready || output == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_drbg_mutex, portMAX_DELAY);
|
||||
int result = mbedtls_ctr_drbg_random(&s_drbg, output, length);
|
||||
xSemaphoreGive(s_drbg_mutex);
|
||||
return result == 0 ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
static bool bytes_are_zero(const uint8_t *data, size_t size)
|
||||
{
|
||||
@@ -130,14 +65,8 @@ static bool security_is_valid(wifi_config_security_t security)
|
||||
|
||||
void wifi_config_secure_wipe(void *data, size_t size)
|
||||
{
|
||||
volatile uint8_t *byte = (volatile uint8_t *)data;
|
||||
|
||||
if (byte == NULL) {
|
||||
return;
|
||||
}
|
||||
while (size-- > 0U) {
|
||||
*byte++ = 0U;
|
||||
}
|
||||
/* Keep the existing Wi-Fi API while using the device-wide wipe primitive. */
|
||||
secure_wipe(data, size);
|
||||
}
|
||||
|
||||
esp_err_t wifi_config_defaults(wifi_app_config_t *config)
|
||||
@@ -179,7 +108,7 @@ esp_err_t wifi_config_defaults(wifi_app_config_t *config)
|
||||
* A 64-character alphabet maps six random bits without modulo bias. The
|
||||
* password is intentionally independent of the public MAC-derived suffix.
|
||||
*/
|
||||
err = fill_credential_random(random_bytes, sizeof(random_bytes));
|
||||
err = secure_random_fill(random_bytes, sizeof(random_bytes));
|
||||
if (err != ESP_OK) {
|
||||
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
|
||||
return err;
|
||||
|
||||
+1
-3
@@ -84,8 +84,6 @@ typedef struct {
|
||||
wifi_config_sta_profile_t profiles[WIFI_CONFIG_STA_PROFILE_COUNT];
|
||||
} wifi_app_config_t;
|
||||
|
||||
/* Seed the credential DRBG before Wi-Fi, Bluetooth, or ADC use. */
|
||||
esp_err_t wifi_config_entropy_init(void);
|
||||
|
||||
/* Generates a unique AP SSID and a fresh random AP password. */
|
||||
esp_err_t wifi_config_defaults(wifi_app_config_t *config);
|
||||
@@ -109,5 +107,5 @@ esp_err_t wifi_config_save(const wifi_app_config_t *config);
|
||||
/* Pass NULL to generate fresh defaults, or supply validated defaults to save. */
|
||||
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults);
|
||||
|
||||
/* Volatile writes prevent ordinary optimization from eliding secret cleanup. */
|
||||
/* Compatibility wrapper around the shared secure_wipe() implementation. */
|
||||
void wifi_config_secure_wipe(void *data, size_t size);
|
||||
|
||||
Reference in New Issue
Block a user