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
+10 -161
View File
@@ -25,7 +25,6 @@
static const uint8_t s_generated_alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static const uint8_t s_admin_username[] = "admin";
static const uint8_t s_ed25519_type[] = "ssh-ed25519";
static const uint8_t s_ecdsa_type[] = "ecdsa-sha2-nistp256";
static const uint8_t s_ecdsa_curve[] = "nistp256";
@@ -58,7 +57,8 @@ typedef struct {
uint32_t version;
uint32_t size;
uint32_t generation;
uint8_t admin_bootstrapped;
/* Retain the v1 wire byte/layout; derived by recount, never policy state. */
uint8_t v1_admin_marker;
uint8_t user_count;
uint8_t admin_count;
uint8_t reserved;
@@ -351,7 +351,7 @@ static esp_err_t validate_database(const stored_database_t *database)
{
if (database->version != USER_DATABASE_SCHEMA_VERSION ||
database->size != sizeof(*database) || database->generation == 0U ||
database->admin_bootstrapped > 1U || database->reserved != 0U) {
database->v1_admin_marker > 1U || database->reserved != 0U) {
return ESP_ERR_INVALID_VERSION;
}
uint8_t users = 0U;
@@ -428,7 +428,7 @@ static esp_err_t validate_database(const stored_database_t *database)
}
}
if (users != database->user_count || admins != database->admin_count ||
(database->admin_bootstrapped != 0U) != (admins > 0U)) {
(database->v1_admin_marker != 0U) != (admins > 0U)) {
return ESP_ERR_INVALID_RESPONSE;
}
return ESP_OK;
@@ -446,6 +446,7 @@ static void recount(stored_database_t *database)
}
}
}
database->v1_admin_marker = database->admin_count > 0U ? 1U : 0U;
}
static esp_err_t next_generation(uint32_t *generation)
@@ -518,58 +519,7 @@ static esp_err_t initialize_user(stored_user_t *user,
return error;
}
static bool legacy_credentials_valid(const user_database_legacy_credentials_t *legacy)
{
return legacy != NULL && legacy->username != NULL && legacy->password != NULL &&
user_database_username_valid(legacy->username, legacy->username_length) &&
user_database_password_valid(legacy->password, legacy->password_length);
}
static esp_err_t synchronize_legacy_locked(
const user_database_legacy_credentials_t *legacy, bool *synchronized)
{
*synchronized = false;
if (s_database.admin_bootstrapped != 0U) {
return ESP_OK;
}
int index = find_user(&s_database, legacy->username, legacy->username_length);
if (index < 0) {
return ESP_OK;
}
const stored_user_t *stored = &s_database.users[index];
uint8_t derived[USER_DATABASE_PASSWORD_HASH_LENGTH] = {0};
esp_err_t error = derive_password(legacy->password, legacy->password_length,
stored->password_salt,
stored->password_iterations, derived);
bool already_current = error == ESP_OK &&
constant_time_equal(derived, stored->password_hash,
sizeof(derived));
secure_wipe(derived, sizeof(derived));
if (error != ESP_OK || already_current) {
*synchronized = already_current;
return error;
}
*s_candidate = s_database;
stored_user_t *candidate_user = &s_candidate->users[index];
error = set_record_password(candidate_user, legacy->password,
legacy->password_length);
if (error == ESP_OK) {
error = next_generation(&candidate_user->auth_generation);
}
if (error == ESP_OK) {
error = commit_candidate_locked();
} else {
discard_candidate();
}
*synchronized = error == ESP_OK;
return error;
}
esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
user_database_load_result_t *load_result)
esp_err_t user_database_init(user_database_load_result_t *load_result)
{
if (load_result == NULL || s_mutex != NULL) {
return ESP_ERR_INVALID_ARG;
@@ -609,10 +559,6 @@ esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
if (error == ESP_OK && !storage_missing) {
error = validate_database(&s_database);
}
if (error == ESP_OK && !storage_missing && legacy_credentials_valid(legacy)) {
bool synchronized = false;
error = synchronize_legacy_locked(legacy, &synchronized);
}
if (error == ESP_OK && !storage_missing) {
error = initialize_dummy_verifier();
if (error == ESP_OK) {
@@ -630,7 +576,7 @@ esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
goto init_failed;
}
if (!storage_missing || !legacy_credentials_valid(legacy)) {
if (!storage_missing) {
error = ESP_ERR_INVALID_STATE;
goto init_failed;
}
@@ -639,13 +585,6 @@ esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
s_database.version = USER_DATABASE_SCHEMA_VERSION;
s_database.size = sizeof(s_database);
s_database.generation = 1U;
error = initialize_user(&s_database.users[0], legacy->username,
legacy->username_length, USER_ROLE_USER,
legacy->password, legacy->password_length);
if (error != ESP_OK) {
goto init_failed;
}
*load_result = USER_DATABASE_LOAD_MIGRATED_LEGACY;
recount(&s_database);
*s_candidate = s_database;
error = commit_candidate_locked();
@@ -669,28 +608,8 @@ init_failed:
return error;
}
esp_err_t user_database_sync_legacy_credentials(
const user_database_legacy_credentials_t *legacy, bool *synchronized)
esp_err_t user_database_recover_empty(void)
{
if (synchronized == NULL || !legacy_credentials_valid(legacy)) {
return ESP_ERR_INVALID_ARG;
}
*synchronized = false;
if (!s_initialized || s_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
esp_err_t error = synchronize_legacy_locked(legacy, synchronized);
xSemaphoreGive(s_mutex);
return error;
}
esp_err_t user_database_recover_from_legacy(
const user_database_legacy_credentials_t *legacy)
{
if (!legacy_credentials_valid(legacy)) {
return ESP_ERR_INVALID_ARG;
}
if (s_initialized || s_mutex != NULL) {
return ESP_ERR_INVALID_STATE;
}
@@ -715,14 +634,7 @@ esp_err_t user_database_recover_from_legacy(
s_candidate->version = USER_DATABASE_SCHEMA_VERSION;
s_candidate->size = sizeof(*s_candidate);
s_candidate->generation = 1U;
error = initialize_user(&s_candidate->users[0], legacy->username,
legacy->username_length, USER_ROLE_USER,
legacy->password, legacy->password_length);
if (error == ESP_OK) {
error = commit_candidate_locked();
} else {
discard_candidate();
}
error = commit_candidate_locked();
if (error == ESP_OK) {
error = initialize_dummy_verifier();
}
@@ -752,7 +664,6 @@ esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot)
memset(snapshot, 0, sizeof(*snapshot));
xSemaphoreTake(s_mutex, portMAX_DELAY);
snapshot->initialized = true;
snapshot->admin_bootstrapped = s_database.admin_bootstrapped != 0U;
snapshot->generation = s_database.generation;
snapshot->user_count = s_database.user_count;
snapshot->admin_count = s_database.admin_count;
@@ -972,9 +883,6 @@ static esp_err_t create_locked(const uint8_t *username, size_t username_length,
*s_candidate = s_database;
esp_err_t error = initialize_user(&s_candidate->users[free_index], username,
username_length, role, password, password_length);
if (error == ESP_OK && role == USER_ROLE_ADMIN) {
s_candidate->admin_bootstrapped = 1U;
}
if (error == ESP_OK) {
return commit_candidate_locked();
}
@@ -1017,56 +925,6 @@ esp_err_t user_database_create_generated(
return error;
}
esp_err_t user_database_bootstrap_admin(const uint8_t *password,
size_t password_length)
{
if (!s_initialized || s_mutex == NULL ||
!user_database_password_valid(password, password_length)) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
if (s_database.admin_bootstrapped != 0U) {
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
int index = find_user(&s_database, s_admin_username, sizeof(s_admin_username) - 1U);
esp_err_t error;
if (index < 0) {
error = create_locked(s_admin_username, sizeof(s_admin_username) - 1U,
USER_ROLE_ADMIN, password, password_length);
} else {
*s_candidate = s_database;
stored_user_t *user = &s_candidate->users[index];
error = set_record_password(user, password, password_length);
if (error == ESP_OK) {
user->role = USER_ROLE_ADMIN;
error = next_generation(&user->auth_generation);
}
if (error == ESP_OK) {
s_candidate->admin_bootstrapped = 1U;
error = commit_candidate_locked();
} else {
discard_candidate();
}
}
xSemaphoreGive(s_mutex);
return error;
}
esp_err_t user_database_bootstrap_admin_generated(
user_database_generated_password_t *generated_password)
{
esp_err_t error = user_database_generate_password_value(generated_password);
if (error == ESP_OK) {
error = user_database_bootstrap_admin(generated_password->password,
generated_password->password_length);
}
if (error != ESP_OK && generated_password != NULL) {
secure_wipe(generated_password, sizeof(*generated_password));
}
return error;
}
static esp_err_t mutate_user_begin(const uint8_t *username, size_t username_length,
int *index)
{
@@ -1141,13 +999,7 @@ static esp_err_t delete_user(const uint8_t *username, size_t username_length,
? mutate_user_begin(username, username_length, &index) : ESP_ERR_NOT_FOUND;
if (error == ESP_OK) {
const stored_user_t *user = &s_database.users[index];
bool protected_migrated_admin =
s_database.admin_bootstrapped == 0U &&
user->username_length == sizeof(s_admin_username) - 1U &&
memcmp(user->username, s_admin_username,
sizeof(s_admin_username) - 1U) == 0;
if (protected_migrated_admin ||
(user->role == USER_ROLE_ADMIN && s_database.admin_count <= 1U)) {
if (user->role == USER_ROLE_ADMIN && s_database.admin_count <= 1U) {
error = ESP_ERR_INVALID_STATE;
discard_candidate();
} else {
@@ -1181,9 +1033,6 @@ static esp_err_t set_role(const uint8_t *username, size_t username_length,
} else {
user->role = (uint8_t)role;
error = next_generation(&user->auth_generation);
if (error == ESP_OK && role == USER_ROLE_ADMIN) {
s_candidate->admin_bootstrapped = 1U;
}
if (error == ESP_OK) {
error = commit_candidate_locked();
} else {