Add Authenticated HTTPS Admin Foundation

This commit is contained in:
2026-08-23 17:37:49 +02:00
parent 0dcc0d20d4
commit 8b5417881c
15 changed files with 2233 additions and 89 deletions
+69 -6
View File
@@ -12,7 +12,7 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
- 8 MB octal PSRAM - 8 MB octal PSRAM
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988 - Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, and the first real broker transport through native USB CDC-ACM. The current phase adds persistent multi-profile Wi-Fi station configuration and managed AP fallback/AP+STA operation. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a host opens native USB CDC. The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, native USB CDC-ACM, and the **Phase 4 Wi-Fi foundation**. The current Phase 5A foundation adds authenticated HTTPS with a persistent device-specific identity and physical-console recovery. WebSocket/xterm.js serial transport is deliberately deferred to the next web phase. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a host opens native USB CDC.
## Hardware wiring ## Hardware wiring
@@ -24,14 +24,14 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
| Partition | Offset | Size | Purpose | | Partition | Offset | Size | Purpose |
|---|---:|---:|---| |---|---:|---:|---|
| `nvs` | `0x009000` | 512 KiB | Serial configuration and future Wi-Fi/provisioning data | | `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, HTTPS credential, and certificate/key data |
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata | | `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data | | `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys | | `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
| `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps | | `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps |
| `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot | | `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot |
| `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot | | `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot |
| `storage` | `0x8B0000` | 7488 KiB | Future LittleFS web assets, certificates, logs, and files | | `storage` | `0x8B0000` | 7488 KiB | Future LittleFS web assets, logs, and files |
Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip. Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
@@ -68,7 +68,7 @@ pio device monitor -b 115200
The firmware starts an interactive console on UART0 with the prompt `serial-tool>`. Type `help` to display concise root-command descriptions. This USB-to-UART device normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport described below. The firmware starts an interactive console on UART0 with the prompt `serial-tool>`. Type `help` to display concise root-command descriptions. This USB-to-UART device normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport described below.
The console provides line editing, history for the current session, terminal-aware cursor movement, and Tab completion. ESP-IDF supplies completion for root commands; the project extends it to nested `debug`, `serial`, `broker`, `usb`, and `wifi` subcommands plus safe fixed values such as AP policy and serial framing. Password values are intentionally never completion candidates. The console provides line editing, history for the current session, terminal-aware cursor movement, and Tab completion. ESP-IDF supplies completion for root commands; the project extends it to nested `debug`, `serial`, `broker`, `usb`, `wifi`, and `web` subcommands plus safe fixed values such as AP policy and serial framing. Password values are intentionally never completion candidates.
ESP-IDF normally probes terminal cursor support once while constructing the UART REPL. If the board boots without a terminal attached, that probe times out and selects dumb mode. The DevKit's USB-to-UART bridge exposes no host-open signal that firmware can monitor, and entering enhanced mode before a terminal exists would block linenoise while it waits for a cursor-position response. ESP-IDF normally probes terminal cursor support once while constructing the UART REPL. If the board boots without a terminal attached, that probe times out and selects dumb mode. The DevKit's USB-to-UART bridge exposes no host-open signal that firmware can monitor, and entering enhanced mode before a terminal exists would block linenoise while it waits for a cursor-position response.
@@ -208,7 +208,7 @@ Power down and remove the DE-9 pin 3-to-2 jumper before connecting an external s
### Wi-Fi foundation ### Wi-Fi foundation
Wi-Fi is managed independently of the serial-session broker in this phase. It provides network connectivity and recovery access-point policy for the later HTTPS/WebSocket transport, but it does not yet run HTTP, HTTPS, DNS interception, a captive portal, NAPT, or any TCP serial listener. Wi-Fi is managed independently of the serial-session broker. It provides network connectivity and recovery access-point policy for the HTTPS service. The firmware does not run plaintext HTTP, DNS interception, a captive portal, NAPT, or any TCP serial listener.
Configuration uses four fixed station-profile slots. Lower numeric priority values are tried first, with slot number breaking ties. Profiles support WPA2/WPA3 mixed operation or require WPA3-SAE. ESP-IDF's station threshold can express “WPA2 or stronger” but not a strict WPA2-only maximum, so the configuration does not pretend to offer a distinct WPA2-only mode. Each profile attempt has a 12-second association/DHCP deadline. After all enabled profiles fail, the manager uses exponential retry delays from 2 to 60 seconds. Configuration uses four fixed station-profile slots. Lower numeric priority values are tried first, with slot number breaking ties. Profiles support WPA2/WPA3 mixed operation or require WPA3-SAE. ESP-IDF's station threshold can express “WPA2 or stronger” but not a strict WPA2-only maximum, so the configuration does not pretend to offer a distinct WPA2-only mode. Each profile attempt has a 12-second association/DHCP deadline. After all enabled profiles fail, the manager uses exponential retry delays from 2 to 60 seconds.
@@ -264,7 +264,7 @@ Wi-Fi credentials currently reside as plaintext in the application-owned `wifi_a
#### Wi-Fi validation #### Wi-Fi validation
1. Boot with no station profiles. `wifi status` should report `ap-only`, and the generated SSID should be visible from another device. 1. Boot with no station profiles. `wifi status` should report `ap-only`, and the generated SSID should be visible from another device.
2. Use `wifi ap show-secret`, join the AP, confirm a `192.168.4.x` lease, and run `wifi ping 192.168.4.1`. No web page is expected yet. 2. Use `wifi ap show-secret`, join the AP, confirm a `192.168.4.x` lease, and run `wifi ping 192.168.4.1`. The authenticated HTTPS page should be reachable at `https://192.168.4.1/`.
3. Configure and enable a WPA2/WPA3 station profile using the example above. `wifi status` should progress through `connecting`, `waiting-ip`, and `online` and display the acquired address, channel, RSSI, and negotiated authentication. 3. Configure and enable a WPA2/WPA3 station profile using the example above. `wifi status` should progress through `connecting`, `waiting-ip`, and `online` and display the acquired address, channel, RSSI, and negotiated authentication.
4. Reboot and verify profile and AP credential persistence. 4. Reboot and verify profile and AP credential persistence.
5. Configure two profiles with different priorities, make the first unavailable, and verify failover to the second after its timeout. 5. Configure two profiles with different priorities, make the first unavailable, and verify failover to the second after its timeout.
@@ -273,6 +273,69 @@ Wi-Fi credentials currently reside as plaintext in the application-owned `wifi_a
8. Test `wifi stop`, `wifi start`, and `wifi reconnect` while confirming UART0 and native USB serial operation remain unaffected. 8. Test `wifi stop`, `wifi start`, and `wifi reconnect` while confirming UART0 and native USB serial operation remain unaffected.
9. If available, test a WPA3-only profile and a wrong password, then inspect the disconnect reason and counters. 9. If available, test a WPA3-only profile and a wrong password, then inspect the disconnect reason and counters.
### Phase 5A authenticated HTTPS foundation
One ESP-IDF HTTPS server listens on TCP port 443 across whichever AP and station interfaces are active. There is no plaintext port 80 listener. This slice intentionally exposes only:
```text
GET /
GET /api/status
```
Both endpoints require HTTP Basic authentication over TLS. `/` is a small self-contained status landing page; `/api/status` returns JSON containing uptime plus non-secret Wi-Fi, serial-service, broker, native-USB, and HTTPS state/counters. No WebSocket, xterm.js terminal, or web broker client exists in Phase 5A.
On first boot, the device generates and persists:
- username `admin` and a random 24-character Base64URL-safe password;
- an ECDSA P-256 private key;
- a device-specific self-signed SHA-256 certificate valid from 2025-01-01 through 2049-12-31;
- certificate SANs for `192.168.4.1` and a MAC-suffixed name such as `esp32-sak-a1b2c3.local`.
The certificate and credentials remain stable across ordinary reboot and OTA-slot changes until explicitly rotated. The `.local` name is included for future hostname discovery, but this phase does not yet advertise mDNS; use the AP address or the station address reported by `wifi status`.
The physical UART0 administration console provides:
```text
web
web help
web status
web start|stop
web counters|clear-counters
web credentials show
web credentials rotate --force
web certificate info
web certificate rotate --force
web reset --force
```
`web` and `web help` print the same usage summary. `web credentials show` is the intended first-boot credential-retrieval path. Rotation and reset operations write NVS immediately rather than creating RAM-only secrets. Destructive operations require a literal `--force`; `web reset --force` is also the recovery path for an incompatible or damaged `web_sec/material` blob and starts HTTPS with the recovered material. Invalid stored material is never overwritten automatically, and HTTPS failure never disables UART0, native USB, the serial core, or Wi-Fi recovery.
The self-signed certificate is not trusted by browsers or host tools by default. After retrieving the password, validate from a host connected to the fallback AP with:
```sh
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' https://192.168.4.1/
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' https://192.168.4.1/api/status
```
A request without credentials should return `401 Unauthorized` and a `WWW-Authenticate` challenge:
```sh
curl -k -i https://192.168.4.1/api/status
```
Inspect and compare the live certificate with `web certificate info`:
```sh
openssl s_client -connect 192.168.4.1:443 -servername esp32-sak-device.local </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -fingerprint -sha256
```
Replace the example SNI name with the DNS SAN printed by `web certificate info`. SNI is not required for this single-certificate server, but supplying the device name makes the test representative of future hostname use. Repeat the fingerprint check after reboot to confirm persistence, then optionally test each explicit rotation command and verify that only the requested material changes.
HTTPS is memory-bounded to two simultaneous client sockets; ESP-IDF documents approximately 40 KiB per TLS socket. Basic authentication is acceptable here only because plaintext HTTP is disabled. It is an initial administration mechanism, not the final authorization design.
**Current security limitation:** the web password and ECDSA private key are stored as plaintext in the application-owned `web_sec/material` NVS blob, just as Wi-Fi credentials are currently plaintext in `wifi_app/config`. The reserved `nvs_key` partition does not activate NVS encryption. ESP-IDF 5.5 also keeps an internal heap copy of the active TLS private key and does not guarantee zeroization when that allocation is freed. Do not treat the current firmware as resistant to physical flash or RAM extraction; NVS encryption, flash encryption, secure boot, protected OTA, secret-aware core-dump handling, and framework-level key zeroization belong to the later hardening phase.
### Phase 0 diagnostics ### Phase 0 diagnostics
The top-level `status` command retains quick MAX3243 signal-state inspection. Potentially disruptive hardware-characterization operations are grouped below `debug` so the primary help page stays concise: The top-level `status` command retains quick MAX3243 signal-state inspection. Potentially disruptive hardware-characterization operations are grouped below `debug` so the primary help page stays concise:
+6
View File
@@ -13,6 +13,12 @@ CONFIG_TINYUSB_CDC_RX_BUFSIZE=1024
CONFIG_TINYUSB_CDC_TX_BUFSIZE=1024 CONFIG_TINYUSB_CDC_TX_BUFSIZE=1024
CONFIG_TINYUSB_CDC_EP_BUFSIZE=512 CONFIG_TINYUSB_CDC_EP_BUFSIZE=512
# Enable the TLS-only administration server; no plaintext HTTP listener is created.
CONFIG_ESP_HTTPS_SERVER_ENABLE=y
# Certificate generation and HTTPS startup use nested cryptographic buffers.
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
# Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP. # Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP.
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
+6
View File
@@ -4,6 +4,7 @@ idf_component_register(
"console_completion.c" "console_completion.c"
"network_console.c" "network_console.c"
"system_console.c" "system_console.c"
"secure_random.c"
"status_led.c" "status_led.c"
"rs232_hw_test.c" "rs232_hw_test.c"
"rs232_port_owner.c" "rs232_port_owner.c"
@@ -14,6 +15,9 @@ idf_component_register(
"session_console.c" "session_console.c"
"usb_cdc_transport.c" "usb_cdc_transport.c"
"usb_console.c" "usb_console.c"
"web_security.c"
"web_server.c"
"web_console.c"
"wifi_config.c" "wifi_config.c"
"wifi_manager.c" "wifi_manager.c"
"wifi_console.c" "wifi_console.c"
@@ -24,6 +28,8 @@ idf_component_register(
esp_driver_gpio esp_driver_gpio
esp_driver_uart esp_driver_uart
esp_event esp_event
esp_http_server
esp_https_server
esp_netif esp_netif
esp_psram esp_psram
esp_system esp_system
+18
View File
@@ -108,6 +108,24 @@ static const char *const s_completion_candidates[] = {
"wifi ping", "wifi ping",
"wifi nslookup", "wifi nslookup",
"wifi traceroute", "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, static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
+47 -5
View File
@@ -7,6 +7,7 @@
#include "network_console.h" #include "network_console.h"
#include "rs232_hw_test.h" #include "rs232_hw_test.h"
#include "rs232_port_owner.h" #include "rs232_port_owner.h"
#include "secure_random.h"
#include "serial_config.h" #include "serial_config.h"
#include "serial_console.h" #include "serial_console.h"
#include "serial_service.h" #include "serial_service.h"
@@ -16,6 +17,9 @@
#include "system_console.h" #include "system_console.h"
#include "usb_cdc_transport.h" #include "usb_cdc_transport.h"
#include "usb_console.h" #include "usb_console.h"
#include "web_console.h"
#include "web_security.h"
#include "web_server.h"
#include "wifi_config.h" #include "wifi_config.h"
#include "wifi_console.h" #include "wifi_console.h"
#include "wifi_manager.h" #include "wifi_manager.h"
@@ -28,7 +32,7 @@ static const char *TAG = "firmware";
void app_main(void) 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()) { if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size()); 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"); ESP_LOGW(TAG, "PSRAM is not initialized");
} }
/* Seed credential generation before any future RF or ADC initialization. */ /* Seed the sole device DRBG before any future RF, Bluetooth, or ADC use. */
esp_err_t wifi_entropy_error = wifi_config_entropy_init(); 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. */ /* Blue means the firmware is initialized and waiting for a console command. */
ESP_ERROR_CHECK(status_led_init()); 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. */ /* Native USB owns GPIO19/20; UART0 logging stays on the USB-to-UART bridge. */
ESP_ERROR_CHECK(usb_cdc_transport_init()); 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_app_config_t wifi_config;
wifi_config_load_source_t wifi_config_source; 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) { if (wifi_config_error == ESP_OK) {
wifi_config_error = wifi_config_load(&wifi_config, &wifi_config_source); 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"); "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) { 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) { if (wifi_error == ESP_OK && wifi_config.enabled_at_boot != 0U) {
wifi_error = wifi_manager_start(); wifi_error = wifi_manager_start();
} }
@@ -103,6 +133,17 @@ void app_main(void)
} }
wifi_config_secure_wipe(&wifi_config, sizeof(wifi_config)); 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( ESP_LOGI(
TAG, TAG,
"Using %s serial configuration; UART service starts on 'serial start' or native USB open", "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(session_console_register_commands());
ESP_ERROR_CHECK(usb_console_register_commands()); ESP_ERROR_CHECK(usb_console_register_commands());
ESP_ERROR_CHECK(wifi_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(network_console_register_root_commands());
ESP_ERROR_CHECK(system_console_register_commands()); ESP_ERROR_CHECK(system_console_register_commands());
/* Upgrade late UART terminals safely and add nested completion. */ /* Upgrade late UART terminals safely and add nested completion. */
+131
View File
@@ -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;
}
}
+32
View File
@@ -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
+303
View File
@@ -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);
}
+8
View File
@@ -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);
+979
View File
@@ -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;
}
+99
View File
@@ -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
+482
View File
@@ -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;
}
+48
View File
@@ -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
View File
@@ -3,18 +3,13 @@
#include "wifi_config.h" #include "wifi_config.h"
#include <limits.h>
#include <stddef.h> #include <stddef.h>
#include <string.h> #include <string.h>
#include "bootloader_random.h"
#include "esp_mac.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.h"
#include "nvs_flash.h" #include "nvs_flash.h"
#include "secure_random.h"
_Static_assert(sizeof(wifi_config_sta_profile_t) == WIFI_CONFIG_STA_PROFILE_BLOB_SIZE, _Static_assert(sizeof(wifi_config_sta_profile_t) == WIFI_CONFIG_STA_PROFILE_BLOB_SIZE,
"Wi-Fi STA profile schema size changed"); "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, _Static_assert(sizeof(wifi_app_config_t) == WIFI_CONFIG_BLOB_SIZE,
"Wi-Fi config schema size changed"); "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) 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) void wifi_config_secure_wipe(void *data, size_t size)
{ {
volatile uint8_t *byte = (volatile uint8_t *)data; /* Keep the existing Wi-Fi API while using the device-wide wipe primitive. */
secure_wipe(data, size);
if (byte == NULL) {
return;
}
while (size-- > 0U) {
*byte++ = 0U;
}
} }
esp_err_t wifi_config_defaults(wifi_app_config_t *config) 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 * A 64-character alphabet maps six random bits without modulo bias. The
* password is intentionally independent of the public MAC-derived suffix. * 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) { if (err != ESP_OK) {
wifi_config_secure_wipe(random_bytes, sizeof(random_bytes)); wifi_config_secure_wipe(random_bytes, sizeof(random_bytes));
return err; return err;
+1 -3
View File
@@ -84,8 +84,6 @@ typedef struct {
wifi_config_sta_profile_t profiles[WIFI_CONFIG_STA_PROFILE_COUNT]; wifi_config_sta_profile_t profiles[WIFI_CONFIG_STA_PROFILE_COUNT];
} wifi_app_config_t; } 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. */ /* Generates a unique AP SSID and a fresh random AP password. */
esp_err_t wifi_config_defaults(wifi_app_config_t *config); 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. */ /* 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); 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); void wifi_config_secure_wipe(void *data, size_t size);