Remove Legacy Credential Bootstrap Paths

Decouple user provisioning from HTTPS identity storage while retaining
compatible v1 user records and migrating TLS material to the
credential-free
v2 format. Add focused security regression coverage and update operator
documentation.
This commit is contained in:
2026-09-08 19:09:26 +02:00
parent 82f21d6116
commit ac80863d80
26 changed files with 1013 additions and 583 deletions
+94 -129
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Canonical NVS storage for HTTPS identity and legacy recovery credentials. */
/* Canonical NVS storage for HTTPS identity with private v1 storage compatibility. */
#include "web_security.h"
@@ -20,12 +20,10 @@
#include "nvs.h"
#include "secure_random.h"
#define WEB_SECURITY_SCHEMA_VERSION 1U
#define WEB_SECURITY_BLOB_SIZE 1392U
#define WEB_SECURITY_SCHEMA_VERSION 2U
#define WEB_SECURITY_BLOB_SIZE 1340U
#define LEGACY_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 {
@@ -33,25 +31,20 @@ typedef struct {
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,
_Static_assert(offsetof(web_security_blob_t, private_key_der) == 16U,
"web security key offset changed");
_Static_assert(offsetof(web_security_blob_t, certificate_der) == 324U,
_Static_assert(offsetof(web_security_blob_t, certificate_der) == 272U,
"web security certificate offset changed");
_Static_assert(offsetof(web_security_blob_t, certificate_fingerprint) == 1296U,
"web security fingerprint offset changed");
_Static_assert(sizeof(web_security_blob_t) == WEB_SECURITY_BLOB_SIZE,
"web security schema size changed");
@@ -116,28 +109,6 @@ static esp_err_t build_device_names(char *common_name, size_t common_name_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)
{
@@ -396,6 +367,16 @@ cleanup:
return valid;
}
static bool der_is_exact_sequence(const uint8_t *der, size_t size)
{
unsigned char *cursor = (unsigned char *)der;
const unsigned char *end = der + size;
size_t length = 0U;
return mbedtls_asn1_get_tag(&cursor, end, &length,
MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0 &&
length == (size_t)(end - cursor);
}
static esp_err_t validate_certificate_and_key(const web_security_blob_t *blob)
{
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY] = {0};
@@ -419,6 +400,10 @@ static esp_err_t validate_certificate_and_key(const web_security_blob_t *blob)
sizeof(fingerprint))) {
goto cleanup;
}
if (!der_is_exact_sequence(blob->private_key_der, blob->private_key_length) ||
!der_is_exact_sequence(blob->certificate_der, blob->certificate_length)) {
goto cleanup;
}
if (mbedtls_pk_parse_key(&private_key,
blob->private_key_der, blob->private_key_length,
NULL, 0U, secure_random_mbedtls, NULL) != 0 ||
@@ -492,16 +477,7 @@ static esp_err_t validate_blob(const web_security_blob_t *blob)
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,
@@ -513,16 +489,6 @@ static esp_err_t validate_blob(const web_security_blob_t *blob)
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);
}
@@ -533,10 +499,7 @@ static esp_err_t generate_all(web_security_blob_t *blob, uint32_t generation)
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);
}
esp_err_t error = generate_certificate(blob);
if (error == ESP_OK) {
error = validate_blob(blob);
}
@@ -556,7 +519,7 @@ static esp_err_t save_blob(const web_security_blob_t *blob)
return error;
}
/* NVS append semantics retain the committed predecessor until commit succeeds. */
/* Publish only after commit. NVS replacement is not secure flash erasure. */
error = nvs_set_blob(handle, WEB_SECURITY_NVS_BLOB_KEY,
blob, sizeof(*blob));
if (error == ESP_OK) {
@@ -566,9 +529,57 @@ static esp_err_t save_blob(const web_security_blob_t *blob)
return error;
}
static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing)
/* The shipped ESP32 v1 wire layout is little-endian, independent of host ABI.
* Credentials exist only in this transient decoder input, never live state. */
static uint16_t legacy_u16(const uint8_t *p)
{
return (uint16_t)p[0] | (uint16_t)((uint16_t)p[1] << 8);
}
static uint32_t legacy_u32(const uint8_t *p)
{
return (uint32_t)legacy_u16(p) | ((uint32_t)legacy_u16(p + 2) << 16);
}
static esp_err_t decode_legacy(const uint8_t raw[LEGACY_BLOB_SIZE],
web_security_blob_t *blob)
{
if (legacy_u32(raw) != 1U || legacy_u16(raw + 4) != LEGACY_BLOB_SIZE) {
return ESP_ERR_INVALID_VERSION;
}
if (legacy_u16(raw + 6) != 0U || legacy_u16(raw + 18) != 0U ||
raw[12] != 5U || raw[13] != 24U ||
memcmp(raw + 20, "admin", 5U) != 0 ||
!bytes_are_zero(raw + 25, 11U) ||
!bytes_are_zero(raw + 60, 8U) ||
!bytes_are_zero(raw + 1380, 12U)) {
return ESP_ERR_INVALID_RESPONSE;
}
for (size_t i = 36U; i < 60U; ++i) {
uint8_t c = raw[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_')) {
return ESP_ERR_INVALID_RESPONSE;
}
}
memset(blob, 0, sizeof(*blob));
blob->schema_version = WEB_SECURITY_SCHEMA_VERSION;
blob->blob_size = WEB_SECURITY_BLOB_SIZE;
blob->generation = legacy_u32(raw + 8);
blob->private_key_length = legacy_u16(raw + 14);
blob->certificate_length = legacy_u16(raw + 16);
memcpy(blob->private_key_der, raw + 68, sizeof(blob->private_key_der));
memcpy(blob->certificate_der, raw + 324, sizeof(blob->certificate_der));
memcpy(blob->certificate_fingerprint, raw + 1348,
sizeof(blob->certificate_fingerprint));
return validate_blob(blob);
}
static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing,
bool *migrated)
{
*missing = false;
*migrated = 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) {
@@ -594,6 +605,21 @@ static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing)
nvs_close(handle);
return error;
}
if (size == LEGACY_BLOB_SIZE) {
uint8_t legacy[LEGACY_BLOB_SIZE] = {0};
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, legacy, &size);
nvs_close(handle);
if (error == ESP_OK) {
error = size == LEGACY_BLOB_SIZE ? decode_legacy(legacy, blob)
: ESP_ERR_INVALID_VERSION;
}
secure_wipe(legacy, sizeof(legacy));
if (error == ESP_OK) {
error = save_blob(blob);
*migrated = error == ESP_OK;
}
return error == ESP_ERR_NVS_INVALID_LENGTH ? ESP_ERR_INVALID_VERSION : error;
}
if (size != sizeof(*blob)) {
nvs_close(handle);
return ESP_ERR_INVALID_VERSION;
@@ -608,7 +634,7 @@ static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing)
if (error != ESP_OK) {
return error;
}
return validate_blob(blob);
return size == sizeof(*blob) ? validate_blob(blob) : ESP_ERR_INVALID_VERSION;
}
esp_err_t web_security_init(web_security_load_result_t *load_result)
@@ -633,7 +659,8 @@ esp_err_t web_security_init(web_security_load_result_t *load_result)
web_security_blob_t candidate;
bool missing = false;
error = load_stored_blob(&candidate, &missing);
bool migrated = false;
error = load_stored_blob(&candidate, &missing, &migrated);
if (error == ESP_OK && missing) {
error = generate_all(&candidate, 1U);
if (error == ESP_OK) {
@@ -644,7 +671,8 @@ esp_err_t web_security_init(web_security_load_result_t *load_result)
s_material = candidate;
s_material_ready = true;
s_load_result = missing ? WEB_SECURITY_LOAD_GENERATED_MISSING
: WEB_SECURITY_LOAD_STORED;
: migrated ? WEB_SECURITY_LOAD_MIGRATED_V1
: WEB_SECURITY_LOAD_STORED;
if (load_result != NULL) {
*load_result = s_load_result;
}
@@ -700,35 +728,6 @@ esp_err_t web_security_copy_tls_material(
}
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)
{
@@ -782,37 +781,6 @@ static void install_committed_blob(const web_security_blob_t *candidate)
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) {
@@ -841,7 +809,7 @@ esp_err_t web_security_rotate_certificate(void)
return error;
}
esp_err_t web_security_reset_all(web_security_credentials_t *new_credentials)
esp_err_t web_security_reset_all(void)
{
esp_err_t error = secure_random_init();
if (error != ESP_OK) {
@@ -869,9 +837,6 @@ esp_err_t web_security_reset_all(web_security_credentials_t *new_credentials)
}
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);