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:
+10
-161
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user