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:
+94
-129
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user