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:
@@ -538,7 +538,7 @@ static bool remote_command_allowed(const admin_request_t *request)
|
||||
/* Empty input is handled quietly by esp_console_run(), not UART0 policy. */
|
||||
bool allowed = true;
|
||||
if (argc >= 2U && strcmp(argv[0], "user") == 0 &&
|
||||
(strcmp(argv[1], "bootstrap") == 0 || strcmp(argv[1], "recover") == 0)) {
|
||||
strcmp(argv[1], "recover") == 0) {
|
||||
allowed = false;
|
||||
}
|
||||
/* Temporary browser policy until lifecycle acknowledgements/revocation are
|
||||
@@ -716,7 +716,7 @@ static void worker_task(void *context)
|
||||
} else if (active) {
|
||||
(void)worker_write(&request.token,
|
||||
request.token.transport == ADMIN_CONSOLE_TRANSPORT_WEB
|
||||
? "Command is unavailable from the web console; use UART0 or SSH where permitted. Bootstrap/recovery require UART0.\r\n"
|
||||
? "Command is unavailable from the web console; use UART0 or SSH where permitted. Recovery requires UART0.\r\n"
|
||||
: "Command is restricted to physical UART0.\r\n");
|
||||
}
|
||||
current = session_is_current(&request.token, &request.principal);
|
||||
|
||||
@@ -126,8 +126,6 @@ static const char *const s_completion_candidates[] = {
|
||||
"user status",
|
||||
"user list",
|
||||
"user show",
|
||||
"user bootstrap",
|
||||
"user bootstrap --generate",
|
||||
"user recover --force",
|
||||
"user add",
|
||||
"user delete",
|
||||
@@ -188,10 +186,6 @@ static const char *const s_completion_candidates[] = {
|
||||
"web diagnostics disable",
|
||||
"web diagnostics show",
|
||||
"web diagnostics clear",
|
||||
"web credentials",
|
||||
"web credentials show",
|
||||
"web credentials rotate",
|
||||
"web credentials rotate --force",
|
||||
"web certificate",
|
||||
"web certificate info",
|
||||
"web certificate rotate",
|
||||
|
||||
+10
-27
@@ -1,5 +1,3 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "admin_ssh_console.h"
|
||||
#include "console_completion.h"
|
||||
@@ -132,29 +130,16 @@ void app_main(void)
|
||||
"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 legacy recovery credential",
|
||||
web_security_source == WEB_SECURITY_LOAD_STORED ? "stored" : "newly generated");
|
||||
ESP_LOGI(TAG, "Using %s HTTPS identity",
|
||||
web_security_source == WEB_SECURITY_LOAD_STORED
|
||||
? "stored"
|
||||
: (web_security_source == WEB_SECURITY_LOAD_MIGRATED_V1
|
||||
? "migrated v1"
|
||||
: "newly generated"));
|
||||
}
|
||||
|
||||
user_database_load_result_t user_database_source = USER_DATABASE_LOAD_EMPTY;
|
||||
web_security_credentials_t legacy_credentials;
|
||||
memset(&legacy_credentials, 0, sizeof(legacy_credentials));
|
||||
user_database_legacy_credentials_t legacy = {0};
|
||||
const user_database_legacy_credentials_t *legacy_pointer = NULL;
|
||||
if (web_security_error == ESP_OK &&
|
||||
web_security_show_credentials(&legacy_credentials) == ESP_OK) {
|
||||
legacy = (user_database_legacy_credentials_t){
|
||||
.username = (const uint8_t *)legacy_credentials.username,
|
||||
.username_length = legacy_credentials.username_length,
|
||||
.password = (const uint8_t *)legacy_credentials.password,
|
||||
.password_length = legacy_credentials.password_length,
|
||||
};
|
||||
legacy_pointer = &legacy;
|
||||
}
|
||||
esp_err_t user_database_error =
|
||||
user_database_init(legacy_pointer, &user_database_source);
|
||||
secure_wipe(&legacy_credentials, sizeof(legacy_credentials));
|
||||
secure_wipe(&legacy, sizeof(legacy));
|
||||
esp_err_t user_database_error = user_database_init(&user_database_source);
|
||||
if (user_database_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "User database unavailable: %s; HTTPS and SSH authentication will fail closed; use UART0 'user recover --force'",
|
||||
esp_err_to_name(user_database_error));
|
||||
@@ -162,9 +147,7 @@ void app_main(void)
|
||||
ESP_LOGI(TAG, "Using %s user database",
|
||||
user_database_source == USER_DATABASE_LOAD_STORED
|
||||
? "stored"
|
||||
: (user_database_source == USER_DATABASE_LOAD_MIGRATED_LEGACY
|
||||
? "newly migrated user-level"
|
||||
: "new empty"));
|
||||
: "new empty");
|
||||
}
|
||||
|
||||
esp_err_t web_runtime_error = web_server_init();
|
||||
@@ -267,8 +250,8 @@ void app_main(void)
|
||||
ESP_LOGI(TAG, "Authenticated HTTPS listening on TCP port 443");
|
||||
}
|
||||
}
|
||||
if (wifi_error == ESP_OK && web_security_error == ESP_OK &&
|
||||
ssh_security_error == ESP_OK && ssh_runtime_error == ESP_OK) {
|
||||
if (wifi_error == ESP_OK && ssh_security_error == ESP_OK &&
|
||||
ssh_runtime_error == ESP_OK) {
|
||||
esp_err_t start_error = ssh_transport_start();
|
||||
if (start_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SSH startup failed: %s; UART0 recovery remains available",
|
||||
|
||||
+8
-62
@@ -15,7 +15,6 @@
|
||||
#include "secure_random.h"
|
||||
#include "ssh_transport.h"
|
||||
#include "user_database.h"
|
||||
#include "web_security.h"
|
||||
#include "web_serial_transport.h"
|
||||
|
||||
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
|
||||
@@ -28,7 +27,6 @@ static void print_usage(void)
|
||||
printf("Usage:\n");
|
||||
printf(" user status|list\n");
|
||||
printf(" user show <username>\n");
|
||||
printf(" user bootstrap [--generate]\n");
|
||||
printf(" user recover --force\n");
|
||||
printf(" user add <username> <user|admin> [--generate]\n");
|
||||
printf(" user delete <username> --force\n");
|
||||
@@ -100,12 +98,11 @@ static int show_users(const char *selected)
|
||||
return 1;
|
||||
}
|
||||
if (selected == NULL) {
|
||||
printf("User database: generation=%lu users=%u/%u admins=%u bootstrapped=%s\n",
|
||||
printf("User database: generation=%lu users=%u/%u admins=%u\n",
|
||||
(unsigned long)s_user_snapshot.generation,
|
||||
(unsigned int)s_user_snapshot.user_count,
|
||||
USER_DATABASE_MAX_USERS,
|
||||
(unsigned int)s_user_snapshot.admin_count,
|
||||
s_user_snapshot.admin_bootstrapped ? "yes" : "no");
|
||||
(unsigned int)s_user_snapshot.admin_count);
|
||||
}
|
||||
bool found = false;
|
||||
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
|
||||
@@ -123,8 +120,8 @@ static int show_users(const char *selected)
|
||||
printf("User '%s' not found.\n", selected);
|
||||
return 1;
|
||||
}
|
||||
if (!s_user_snapshot.admin_bootstrapped) {
|
||||
printf("Administrative network access is not bootstrapped; use 'user bootstrap'.\n");
|
||||
if (s_user_snapshot.admin_count == 0U) {
|
||||
printf("No administrators; use 'user add <username> admin' on UART0.\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -170,52 +167,13 @@ static void show_generated_password(const char *username,
|
||||
|
||||
static int recover_database(void)
|
||||
{
|
||||
web_security_credentials_t credentials;
|
||||
memset(&credentials, 0, sizeof(credentials));
|
||||
esp_err_t error = web_security_show_credentials(&credentials);
|
||||
if (error == ESP_OK) {
|
||||
const user_database_legacy_credentials_t legacy = {
|
||||
.username = (const uint8_t *)credentials.username,
|
||||
.username_length = credentials.username_length,
|
||||
.password = (const uint8_t *)credentials.password,
|
||||
.password_length = credentials.password_length,
|
||||
};
|
||||
error = user_database_recover_from_legacy(&legacy);
|
||||
}
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
esp_err_t error = user_database_recover_empty();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not recover user database: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("User database replaced from the current legacy network credential.\n");
|
||||
printf("The imported account has role user; run 'user bootstrap' to establish an administrator.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bootstrap(bool generated)
|
||||
{
|
||||
esp_err_t error;
|
||||
if (generated) {
|
||||
user_database_generated_password_t password;
|
||||
error = user_database_bootstrap_admin_generated(&password);
|
||||
if (error == ESP_OK) {
|
||||
show_generated_password("admin", &password);
|
||||
}
|
||||
} else {
|
||||
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
|
||||
size_t password_length = 0U;
|
||||
error = read_password(password, &password_length);
|
||||
if (error == ESP_OK) {
|
||||
error = user_database_bootstrap_admin(password, password_length);
|
||||
}
|
||||
secure_wipe(password, sizeof(password));
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not bootstrap administrator: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
revoke_user_network_sessions("admin");
|
||||
printf("Administrator account bootstrapped. Role-aware HTTPS and SSH authentication is active.\n");
|
||||
printf("User database rebuilt empty; no credentials imported.\n");
|
||||
printf("Use 'user add <username> admin' on UART0 to create an administrator.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -422,18 +380,6 @@ static int command_user_inner(int argc, char **argv)
|
||||
}
|
||||
return recover_database();
|
||||
}
|
||||
if ((argc == 2 || argc == 3) && strcmp(argv[1], "bootstrap") == 0) {
|
||||
bool generated = argc == 3 && strcmp(argv[2], "--generate") == 0;
|
||||
if (argc == 3 && !generated) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
if (remote) {
|
||||
printf("Administrator bootstrap is restricted to physical UART0.\n");
|
||||
return 1;
|
||||
}
|
||||
return bootstrap(generated);
|
||||
}
|
||||
if ((argc == 4 || argc == 5) && strcmp(argv[1], "add") == 0) {
|
||||
bool generated = argc == 5 && strcmp(argv[4], "--generate") == 0;
|
||||
if (argc == 5 && !generated) {
|
||||
@@ -449,7 +395,7 @@ static int command_user_inner(int argc, char **argv)
|
||||
error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not delete user (the migrated or final admin is protected): %s\n",
|
||||
printf("Could not delete user (the final admin is protected): %s\n",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
+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 {
|
||||
|
||||
+6
-25
@@ -38,17 +38,9 @@ typedef enum {
|
||||
|
||||
typedef enum {
|
||||
USER_DATABASE_LOAD_STORED = 0,
|
||||
USER_DATABASE_LOAD_MIGRATED_LEGACY,
|
||||
USER_DATABASE_LOAD_EMPTY,
|
||||
} user_database_load_result_t;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *username;
|
||||
size_t username_length;
|
||||
const uint8_t *password;
|
||||
size_t password_length;
|
||||
} user_database_legacy_credentials_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t user_id;
|
||||
uint32_t auth_generation;
|
||||
@@ -84,25 +76,18 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
bool initialized;
|
||||
bool admin_bootstrapped;
|
||||
uint32_t generation;
|
||||
uint8_t user_count;
|
||||
uint8_t admin_count;
|
||||
user_database_user_snapshot_t users[USER_DATABASE_MAX_USERS];
|
||||
} user_database_snapshot_t;
|
||||
|
||||
esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
|
||||
user_database_load_result_t *load_result);
|
||||
/*
|
||||
* Before the first administrator is established, keep the migrated account in
|
||||
* sync with the legacy recovery credential. Once bootstrapped, that credential
|
||||
* remains independent and no longer authenticates Phase 8B network services.
|
||||
*/
|
||||
esp_err_t user_database_sync_legacy_credentials(
|
||||
const user_database_legacy_credentials_t *legacy, bool *synchronized);
|
||||
/* Explicit UART0 recovery: replace unavailable user storage with one legacy user. */
|
||||
esp_err_t user_database_recover_from_legacy(
|
||||
const user_database_legacy_credentials_t *legacy);
|
||||
/* Missing storage is persisted empty; valid v1 records load unchanged.
|
||||
* Corrupt/unsupported storage fails closed and is never automatically replaced. */
|
||||
esp_err_t user_database_init(user_database_load_result_t *load_result);
|
||||
/* Explicit UART0 recovery only; refuses an initialized database. No credentials
|
||||
* are imported or created. Caller enforces physical-console authorization. */
|
||||
esp_err_t user_database_recover_empty(void);
|
||||
esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot);
|
||||
|
||||
/* Compact secret-free list, zero-wait mutex acquisition; no key material. */
|
||||
@@ -149,10 +134,6 @@ esp_err_t user_database_authorize_ssh_public_key(
|
||||
esp_err_t user_database_principal_is_current(const user_principal_t *principal,
|
||||
bool *current);
|
||||
|
||||
esp_err_t user_database_bootstrap_admin(const uint8_t *password,
|
||||
size_t password_length);
|
||||
esp_err_t user_database_bootstrap_admin_generated(
|
||||
user_database_generated_password_t *generated_password);
|
||||
esp_err_t user_database_create(const uint8_t *username, size_t username_length,
|
||||
user_role_t role,
|
||||
const uint8_t *password, size_t password_length);
|
||||
|
||||
+6
-101
@@ -1,5 +1,5 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* UART0 HTTPS lifecycle, legacy recovery credential, and certificate commands. */
|
||||
/* HTTPS lifecycle and TLS certificate commands. */
|
||||
|
||||
#include "web_console.h"
|
||||
#include "admin_ssh_console.h"
|
||||
@@ -9,8 +9,6 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_console.h"
|
||||
#include "secure_random.h"
|
||||
#include "ssh_transport.h"
|
||||
#include "user_database.h"
|
||||
#include "web_security.h"
|
||||
#include "web_serial_transport.h"
|
||||
@@ -26,11 +24,9 @@ static void print_usage(void)
|
||||
printf(" web status|start|stop\n");
|
||||
printf(" web counters|clear-counters\n");
|
||||
printf(" web diagnostics enable|disable|show|clear\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");
|
||||
printf(" web reset --force (TLS certificate and private key only)\n");
|
||||
}
|
||||
|
||||
static void print_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH])
|
||||
@@ -207,25 +203,6 @@ static int show_counters(void)
|
||||
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("Phase 8B uses the user database for HTTPS and SSH authentication.\n");
|
||||
printf("This legacy credential is retained only for migration and physical recovery.\n");
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int show_certificate(void)
|
||||
{
|
||||
web_security_certificate_metadata_t metadata;
|
||||
@@ -276,60 +253,6 @@ static int restart_if_running(bool was_running)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void synchronize_migrated_user(
|
||||
const web_security_credentials_t *credentials)
|
||||
{
|
||||
const user_database_legacy_credentials_t legacy = {
|
||||
.username = (const uint8_t *)credentials->username,
|
||||
.username_length = credentials->username_length,
|
||||
.password = (const uint8_t *)credentials->password,
|
||||
.password_length = credentials->password_length,
|
||||
};
|
||||
bool synchronized = false;
|
||||
esp_err_t error = user_database_sync_legacy_credentials(&legacy, &synchronized);
|
||||
if (error != ESP_OK) {
|
||||
printf("Warning: migrated user synchronization failed: %s. Boot will retry a valid stored database; otherwise use 'user recover --force'.\n",
|
||||
esp_err_to_name(error));
|
||||
return;
|
||||
}
|
||||
if (synchronized) {
|
||||
(void)web_serial_transport_revoke_user(
|
||||
(const uint8_t *)credentials->username,
|
||||
credentials->username_length);
|
||||
(void)ssh_transport_revoke_user(
|
||||
(const uint8_t *)credentials->username,
|
||||
credentials->username_length);
|
||||
printf("The pre-bootstrap migrated user credential was synchronized.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
user_database_snapshot_t snapshot;
|
||||
if (user_database_get_snapshot(&snapshot) == ESP_OK &&
|
||||
snapshot.admin_bootstrapped) {
|
||||
printf("This legacy recovery credential is separate from role-based user passwords.\n");
|
||||
} else {
|
||||
printf("Warning: no matching pre-bootstrap migrated user was synchronized; establish an administrator with 'user bootstrap'.\n");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
synchronize_migrated_user(&credentials);
|
||||
printf("Legacy migration/recovery credential rotated 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));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rotate_certificate(void)
|
||||
{
|
||||
web_server_snapshot_t snapshot;
|
||||
@@ -351,19 +274,13 @@ 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);
|
||||
esp_err_t error = web_security_reset_all();
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not reset web security material: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
synchronize_migrated_user(&credentials);
|
||||
printf("Legacy recovery credential, HTTPS certificate, and HTTPS 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));
|
||||
printf("HTTPS certificate and private key replaced and persisted; user accounts unchanged.\n");
|
||||
if (was_running) {
|
||||
return restart_if_running(true);
|
||||
}
|
||||
@@ -434,18 +351,6 @@ static int command_web(int argc, char **argv)
|
||||
printf("HTTPS and WebSocket 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();
|
||||
@@ -470,7 +375,7 @@ static int command_web(int argc, char **argv)
|
||||
}
|
||||
if (strcmp(argv[1], "reset") == 0) {
|
||||
if (!force_is_present(argc, argv, 3)) {
|
||||
printf("Full material replacement requires: web reset --force\n");
|
||||
printf("TLS-only certificate/private-key replacement requires: web reset --force\n");
|
||||
return 1;
|
||||
}
|
||||
return reset_material();
|
||||
@@ -484,7 +389,7 @@ 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",
|
||||
.help = "Manage authenticated HTTPS and recover TLS certificate/private key",
|
||||
.hint = NULL,
|
||||
.func = &command_web,
|
||||
.argtable = NULL,
|
||||
|
||||
+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);
|
||||
|
||||
+14
-20
@@ -1,5 +1,5 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Persistent HTTPS identity and legacy migration/recovery credentials. */
|
||||
/* Persistent HTTPS identity; authentication belongs to the user database. */
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -16,9 +16,7 @@ extern "C" {
|
||||
#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
|
||||
@@ -31,18 +29,9 @@ extern "C" {
|
||||
typedef enum {
|
||||
WEB_SECURITY_LOAD_STORED = 0,
|
||||
WEB_SECURITY_LOAD_GENERATED_MISSING = 1,
|
||||
WEB_SECURITY_LOAD_MIGRATED_V1 = 2,
|
||||
} 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;
|
||||
@@ -55,7 +44,11 @@ typedef struct {
|
||||
} web_security_certificate_metadata_t;
|
||||
|
||||
/*
|
||||
* NVS must already be initialized. Missing material is generated and saved;
|
||||
* NVS must already be initialized. Missing TLS material is generated and saved.
|
||||
* Valid v1 material is migrated to certificate-only v2 before publication,
|
||||
* preserving exact TLS identity and generation. Replacement is logical NVS
|
||||
* deletion of legacy fields, not secure flash erasure. Migration failure never
|
||||
* triggers regeneration or fallback overwrite;
|
||||
* 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
|
||||
@@ -73,17 +66,18 @@ esp_err_t web_security_copy_tls_material(
|
||||
uint8_t *private_key, size_t private_key_capacity,
|
||||
size_t *private_key_length);
|
||||
|
||||
/* 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);
|
||||
/* TLS ONLY: explicitly replaces missing, valid, or incompatible material.
|
||||
* Generation increments from live state, or starts at one if unavailable.
|
||||
* No user database mutation. */
|
||||
esp_err_t web_security_reset_all(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user