Add role-based user database administration
This commit is contained in:
@@ -2,6 +2,7 @@ idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"console_completion.c"
|
||||
"console_input.c"
|
||||
"network_console.c"
|
||||
"system_console.c"
|
||||
"secure_random.c"
|
||||
@@ -24,6 +25,8 @@ idf_component_register(
|
||||
"ssh_console.c"
|
||||
"usb_cdc_transport.c"
|
||||
"usb_console.c"
|
||||
"user_database.c"
|
||||
"user_console.c"
|
||||
"web_security.c"
|
||||
"web_serial_transport.c"
|
||||
"web_assets_data.c"
|
||||
|
||||
@@ -116,6 +116,21 @@ static const char *const s_completion_candidates[] = {
|
||||
"usb request-writer",
|
||||
"usb release-writer",
|
||||
|
||||
/* Physical role-based user, password, and SSH-key administration. */
|
||||
"user status",
|
||||
"user list",
|
||||
"user show",
|
||||
"user bootstrap",
|
||||
"user bootstrap --generate",
|
||||
"user recover --force",
|
||||
"user add",
|
||||
"user delete",
|
||||
"user role",
|
||||
"user password",
|
||||
"user key add",
|
||||
"user key delete",
|
||||
"user key clear",
|
||||
|
||||
/* Wi-Fi lifecycle, persistence, profiles, AP policy, and diagnostics. */
|
||||
"wifi status",
|
||||
"wifi profiles",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Bounded UART0 input helpers for physical-administration prompts. */
|
||||
|
||||
#include "console_input.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "secure_random.h"
|
||||
|
||||
#define CONSOLE_INPUT_UART UART_NUM_0
|
||||
|
||||
static esp_err_t prepare_prompt(const char *prompt)
|
||||
{
|
||||
vTaskDelay(1U);
|
||||
esp_err_t error = uart_flush_input(CONSOLE_INPUT_UART);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
printf("%s", prompt);
|
||||
fflush(stdout);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t read_input(const char *prompt, uint8_t *output, size_t capacity,
|
||||
bool hidden, size_t *output_length)
|
||||
{
|
||||
if (prompt == NULL || output == NULL || output_length == NULL || capacity == 0U) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*output_length = 0U;
|
||||
memset(output, 0, capacity);
|
||||
esp_err_t error = prepare_prompt(prompt);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint8_t byte = 0U;
|
||||
if (uart_read_bytes(CONSOLE_INPUT_UART, &byte, 1U, portMAX_DELAY) != 1) {
|
||||
secure_wipe(output, capacity);
|
||||
*output_length = 0U;
|
||||
printf("\nInput failed.\n");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (byte == 0x03U) {
|
||||
secure_wipe(output, capacity);
|
||||
*output_length = 0U;
|
||||
printf("\nCancelled.\n");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (byte == '\r' || byte == '\n') {
|
||||
break;
|
||||
}
|
||||
if (byte == 0x08U || byte == 0x7fU) {
|
||||
if (*output_length > 0U) {
|
||||
output[--*output_length] = 0U;
|
||||
if (!hidden) {
|
||||
printf("\b \b");
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (byte < 0x20U || byte > 0x7eU || *output_length >= capacity - 1U) {
|
||||
putchar('\a');
|
||||
fflush(stdout);
|
||||
continue;
|
||||
}
|
||||
output[(*output_length)++] = byte;
|
||||
if (!hidden) {
|
||||
putchar((int)byte);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
putchar('\n');
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t console_input_read_hidden(const char *prompt,
|
||||
uint8_t *output, size_t capacity,
|
||||
size_t minimum_length, size_t maximum_length,
|
||||
size_t *output_length)
|
||||
{
|
||||
if (minimum_length > maximum_length || maximum_length >= capacity) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_err_t error = read_input(prompt, output, capacity, true, output_length);
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
if (*output_length < minimum_length || *output_length > maximum_length) {
|
||||
secure_wipe(output, capacity);
|
||||
*output_length = 0U;
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t console_input_read_line(const char *prompt,
|
||||
uint8_t *output, size_t capacity,
|
||||
size_t *output_length)
|
||||
{
|
||||
return read_input(prompt, output, capacity, false, output_length);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Bounded UART0 input helpers for physical-administration prompts. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t console_input_read_hidden(const char *prompt,
|
||||
uint8_t *output, size_t capacity,
|
||||
size_t minimum_length, size_t maximum_length,
|
||||
size_t *output_length);
|
||||
esp_err_t console_input_read_line(const char *prompt,
|
||||
uint8_t *output, size_t capacity,
|
||||
size_t *output_length);
|
||||
+36
@@ -1,3 +1,5 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "console_completion.h"
|
||||
#include "esp_console.h"
|
||||
@@ -26,6 +28,8 @@
|
||||
#include "system_console.h"
|
||||
#include "usb_cdc_transport.h"
|
||||
#include "usb_console.h"
|
||||
#include "user_console.h"
|
||||
#include "user_database.h"
|
||||
#include "web_console.h"
|
||||
#include "web_security.h"
|
||||
#include "web_server.h"
|
||||
@@ -127,6 +131,37 @@ void app_main(void)
|
||||
web_security_source == WEB_SECURITY_LOAD_STORED ? "stored" : "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));
|
||||
if (user_database_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "User database unavailable: %s; current network authentication remains active",
|
||||
esp_err_to_name(user_database_error));
|
||||
} else {
|
||||
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"));
|
||||
}
|
||||
|
||||
esp_err_t web_runtime_error = web_server_init();
|
||||
if (web_runtime_error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTPS runtime initialization failed: %s",
|
||||
@@ -261,6 +296,7 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(serial_console_register_commands());
|
||||
ESP_ERROR_CHECK(session_console_register_commands());
|
||||
ESP_ERROR_CHECK(usb_console_register_commands());
|
||||
ESP_ERROR_CHECK(user_console_register_commands());
|
||||
ESP_ERROR_CHECK(wifi_console_register_commands());
|
||||
ESP_ERROR_CHECK(web_console_register_commands());
|
||||
ESP_ERROR_CHECK(ssh_console_register_commands());
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Physical UART0 role-based user administration. */
|
||||
|
||||
#include "user_console.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "console_input.h"
|
||||
#include "esp_console.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "secure_random.h"
|
||||
#include "user_database.h"
|
||||
#include "web_security.h"
|
||||
|
||||
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
|
||||
|
||||
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");
|
||||
printf(" user role <username> <user|admin> --force\n");
|
||||
printf(" user password <username> [--generate]\n");
|
||||
printf(" user key add <username>\n");
|
||||
printf(" user key delete <username> <0..2> --force\n");
|
||||
printf(" user key clear <username> --force\n");
|
||||
}
|
||||
|
||||
static void print_fingerprint(const uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH])
|
||||
{
|
||||
uint8_t encoded[48] = {0};
|
||||
size_t length = 0U;
|
||||
if (mbedtls_base64_encode(encoded, sizeof(encoded), &length,
|
||||
fingerprint, USER_DATABASE_SHA256_LENGTH) != 0) {
|
||||
printf("unavailable");
|
||||
return;
|
||||
}
|
||||
while (length > 0U && encoded[length - 1U] == '=') {
|
||||
--length;
|
||||
}
|
||||
printf("SHA256:%.*s", (int)length, (const char *)encoded);
|
||||
secure_wipe(encoded, sizeof(encoded));
|
||||
}
|
||||
|
||||
static void print_user(const user_database_user_snapshot_t *user)
|
||||
{
|
||||
printf("%.*s role=%s id=%lu generation=%lu keys=%u\n",
|
||||
(int)user->username_length, user->username,
|
||||
user_role_to_string(user->role),
|
||||
(unsigned long)user->user_id,
|
||||
(unsigned long)user->auth_generation,
|
||||
(unsigned int)user->public_key_count);
|
||||
for (size_t index = 0U; index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++index) {
|
||||
const user_database_key_snapshot_t *key = &user->public_keys[index];
|
||||
if (!key->active) {
|
||||
continue;
|
||||
}
|
||||
printf(" key %u %.*s ", (unsigned int)key->index,
|
||||
(int)key->key_type_length, key->key_type);
|
||||
print_fingerprint(key->sha256_fingerprint);
|
||||
putchar('\n');
|
||||
}
|
||||
}
|
||||
|
||||
static int show_users(const char *selected)
|
||||
{
|
||||
user_database_snapshot_t snapshot;
|
||||
esp_err_t error = user_database_get_snapshot(&snapshot);
|
||||
if (error != ESP_OK) {
|
||||
printf("User database unavailable: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
if (selected == NULL) {
|
||||
printf("User database: generation=%lu users=%u/%u admins=%u bootstrapped=%s\n",
|
||||
(unsigned long)snapshot.generation,
|
||||
(unsigned int)snapshot.user_count,
|
||||
USER_DATABASE_MAX_USERS,
|
||||
(unsigned int)snapshot.admin_count,
|
||||
snapshot.admin_bootstrapped ? "yes" : "no");
|
||||
}
|
||||
bool found = false;
|
||||
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
|
||||
const user_database_user_snapshot_t *user = &snapshot.users[index];
|
||||
if (!user->active ||
|
||||
(selected != NULL &&
|
||||
(strlen(selected) != user->username_length ||
|
||||
memcmp(selected, user->username, user->username_length) != 0))) {
|
||||
continue;
|
||||
}
|
||||
print_user(user);
|
||||
found = true;
|
||||
}
|
||||
if (selected != NULL && !found) {
|
||||
printf("User '%s' not found.\n", selected);
|
||||
return 1;
|
||||
}
|
||||
if (!snapshot.admin_bootstrapped) {
|
||||
printf("Administrative network access is not bootstrapped; use 'user bootstrap'.\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static esp_err_t read_password(uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U],
|
||||
size_t *password_length)
|
||||
{
|
||||
uint8_t confirmation[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
|
||||
size_t confirmation_length = 0U;
|
||||
esp_err_t error = console_input_read_hidden(
|
||||
"Password (12..64 printable characters, Ctrl-C cancels): ",
|
||||
password, USER_DATABASE_PASSWORD_CAPACITY + 1U,
|
||||
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
|
||||
password_length);
|
||||
if (error == ESP_OK) {
|
||||
error = console_input_read_hidden(
|
||||
"Repeat password: ", confirmation, sizeof(confirmation),
|
||||
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
|
||||
&confirmation_length);
|
||||
}
|
||||
if (error == ESP_OK &&
|
||||
(*password_length != confirmation_length ||
|
||||
memcmp(password, confirmation, *password_length) != 0)) {
|
||||
printf("Passwords do not match.\n");
|
||||
error = ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
secure_wipe(confirmation, sizeof(confirmation));
|
||||
if (error != ESP_OK) {
|
||||
secure_wipe(password, USER_DATABASE_PASSWORD_CAPACITY + 1U);
|
||||
*password_length = 0U;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static void show_generated_password(const char *username,
|
||||
user_database_generated_password_t *generated)
|
||||
{
|
||||
printf("Generated password for %s: %.*s\n",
|
||||
username, (int)generated->password_length, generated->password);
|
||||
printf("This password is shown once; store it securely.\n");
|
||||
secure_wipe(generated, sizeof(*generated));
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
printf("Administrator account bootstrapped. Authentication integration follows in Phase 8B.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_user(const char *username, const char *role_text, bool generated)
|
||||
{
|
||||
user_role_t role;
|
||||
if (!user_database_username_valid((const uint8_t *)username, strlen(username)) ||
|
||||
!user_role_parse(role_text, &role)) {
|
||||
printf("Username must match [a-z][a-z0-9_-]{0,15}; role is user or admin.\n");
|
||||
return 1;
|
||||
}
|
||||
esp_err_t error;
|
||||
if (generated) {
|
||||
user_database_generated_password_t password;
|
||||
error = user_database_create_generated((const uint8_t *)username,
|
||||
strlen(username), role, &password);
|
||||
if (error == ESP_OK) {
|
||||
show_generated_password(username, &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_create((const uint8_t *)username, strlen(username),
|
||||
role, password, password_length);
|
||||
}
|
||||
secure_wipe(password, sizeof(password));
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not add user: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("User '%s' added with role %s.\n", username, user_role_to_string(role));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int change_password(const char *username, bool generated)
|
||||
{
|
||||
esp_err_t error;
|
||||
if (generated) {
|
||||
user_database_generated_password_t password;
|
||||
error = user_database_generate_password((const uint8_t *)username,
|
||||
strlen(username), &password);
|
||||
if (error == ESP_OK) {
|
||||
show_generated_password(username, &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_set_password((const uint8_t *)username,
|
||||
strlen(username),
|
||||
password, password_length);
|
||||
}
|
||||
secure_wipe(password, sizeof(password));
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not change password: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("Password changed; affected network sessions will be revoked in Phase 8B.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool parse_key_index(const char *text, uint8_t *index)
|
||||
{
|
||||
if (text == NULL || text[0] < '0' || text[0] > '9' || text[1] != '\0') {
|
||||
return false;
|
||||
}
|
||||
uint8_t parsed = (uint8_t)(text[0] - '0');
|
||||
if (parsed >= USER_DATABASE_MAX_SSH_KEYS_PER_USER) {
|
||||
return false;
|
||||
}
|
||||
*index = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool key_delimiter(uint8_t value)
|
||||
{
|
||||
return value == ' ' || value == '\t';
|
||||
}
|
||||
|
||||
static int add_key(const char *username)
|
||||
{
|
||||
uint8_t line[USER_CONSOLE_KEY_LINE_CAPACITY] = {0};
|
||||
size_t line_length = 0U;
|
||||
esp_err_t error = console_input_read_line(
|
||||
"OpenSSH public key (type base64 [comment], Ctrl-C cancels): ",
|
||||
line, sizeof(line), &line_length);
|
||||
if (error != ESP_OK) {
|
||||
secure_wipe(line, sizeof(line));
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t *space = NULL;
|
||||
for (size_t index = 0U; index < line_length; ++index) {
|
||||
if (key_delimiter(line[index])) {
|
||||
space = &line[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (space == NULL) {
|
||||
printf("Public key must contain a key type and Base64 blob.\n");
|
||||
secure_wipe(line, sizeof(line));
|
||||
return 1;
|
||||
}
|
||||
size_t type_length = (size_t)(space - line);
|
||||
uint8_t *encoded = space + 1U;
|
||||
size_t remaining = line_length - type_length - 1U;
|
||||
while (remaining > 0U && key_delimiter(*encoded)) {
|
||||
++encoded;
|
||||
--remaining;
|
||||
}
|
||||
uint8_t *encoded_end = NULL;
|
||||
for (size_t index = 0U; index < remaining; ++index) {
|
||||
if (key_delimiter(encoded[index])) {
|
||||
encoded_end = &encoded[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t encoded_length = encoded_end == NULL
|
||||
? remaining
|
||||
: (size_t)(encoded_end - encoded);
|
||||
uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY] = {0};
|
||||
size_t blob_length = 0U;
|
||||
int decoded = mbedtls_base64_decode(blob, sizeof(blob), &blob_length,
|
||||
encoded, encoded_length);
|
||||
if (decoded != 0 ||
|
||||
!user_database_key_valid(line, type_length, blob, blob_length)) {
|
||||
printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n");
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
secure_wipe(line, sizeof(line));
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t key_index = 0U;
|
||||
error = user_database_add_ssh_key((const uint8_t *)username, strlen(username),
|
||||
line, type_length, blob, blob_length, &key_index);
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
secure_wipe(line, sizeof(line));
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not add SSH key: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("SSH public key added at index %u. Key login is enabled in Phase 8B.\n",
|
||||
(unsigned int)key_index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int command_user(int argc, char **argv)
|
||||
{
|
||||
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0) ||
|
||||
(argc == 2 && strcmp(argv[1], "list") == 0)) {
|
||||
return show_users(NULL);
|
||||
}
|
||||
if (argc == 3 && strcmp(argv[1], "show") == 0) {
|
||||
return show_users(argv[2]);
|
||||
}
|
||||
if (argc == 3 && strcmp(argv[1], "recover") == 0 &&
|
||||
strcmp(argv[2], "--force") == 0) {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
return add_user(argv[2], argv[3], generated);
|
||||
}
|
||||
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
|
||||
strcmp(argv[3], "--force") == 0) {
|
||||
esp_err_t 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",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("User '%s' deleted.\n", argv[2]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (argc == 5 && strcmp(argv[1], "role") == 0 &&
|
||||
strcmp(argv[4], "--force") == 0) {
|
||||
user_role_t role;
|
||||
if (!user_role_parse(argv[3], &role)) {
|
||||
printf("Role must be user or admin.\n");
|
||||
return 1;
|
||||
}
|
||||
esp_err_t error = user_database_set_role((const uint8_t *)argv[2],
|
||||
strlen(argv[2]), role);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not change role (the final admin is protected): %s\n",
|
||||
esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("User '%s' role changed to %s.\n", argv[2], user_role_to_string(role));
|
||||
return 0;
|
||||
}
|
||||
if ((argc == 3 || argc == 4) && strcmp(argv[1], "password") == 0) {
|
||||
bool generated = argc == 4 && strcmp(argv[3], "--generate") == 0;
|
||||
if (argc == 4 && !generated) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
return change_password(argv[2], generated);
|
||||
}
|
||||
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "add") == 0) {
|
||||
return add_key(argv[3]);
|
||||
}
|
||||
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "delete") == 0 && strcmp(argv[5], "--force") == 0) {
|
||||
uint8_t index;
|
||||
if (!parse_key_index(argv[4], &index)) {
|
||||
printf("Key index must be 0..2.\n");
|
||||
return 1;
|
||||
}
|
||||
esp_err_t error = user_database_remove_ssh_key(
|
||||
(const uint8_t *)argv[3], strlen(argv[3]), index);
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not delete SSH key: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("SSH key %u deleted for '%s'.\n", (unsigned int)index, argv[3]);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 5 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "clear") == 0 && strcmp(argv[4], "--force") == 0) {
|
||||
esp_err_t error = user_database_clear_ssh_keys(
|
||||
(const uint8_t *)argv[3], strlen(argv[3]));
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not clear SSH keys: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
printf("SSH keys cleared for '%s'.\n", argv[3]);
|
||||
return 0;
|
||||
}
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
esp_err_t user_console_register_commands(void)
|
||||
{
|
||||
const esp_console_cmd_t command = {
|
||||
.command = "user",
|
||||
.help = "Manage bounded role-based users, passwords, and SSH public keys",
|
||||
.hint = NULL,
|
||||
.func = &command_user,
|
||||
.argtable = NULL,
|
||||
};
|
||||
return esp_console_cmd_register(&command);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Physical UART0 role-based user administration. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t user_console_register_commands(void);
|
||||
+1274
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Bounded persistent role-based user and SSH authorized-key database. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define USER_DATABASE_NVS_NAMESPACE "user_db"
|
||||
#define USER_DATABASE_NVS_BLOB_KEY "database"
|
||||
#define USER_DATABASE_MAX_USERS 8U
|
||||
#define USER_DATABASE_MAX_SSH_KEYS_PER_USER 3U
|
||||
#define USER_DATABASE_USERNAME_CAPACITY 16U
|
||||
#define USER_DATABASE_PASSWORD_CAPACITY 64U
|
||||
#define USER_DATABASE_PASSWORD_MIN_LENGTH 12U
|
||||
#define USER_DATABASE_GENERATED_PASSWORD_LENGTH 24U
|
||||
#define USER_DATABASE_SSH_KEY_TYPE_CAPACITY 32U
|
||||
#define USER_DATABASE_SSH_KEY_BLOB_CAPACITY 128U
|
||||
#define USER_DATABASE_SHA256_LENGTH 32U
|
||||
|
||||
typedef enum {
|
||||
USER_ROLE_USER = 1,
|
||||
USER_ROLE_ADMIN = 2,
|
||||
} user_role_t;
|
||||
|
||||
typedef enum {
|
||||
USER_AUTH_METHOD_PASSWORD = 1,
|
||||
USER_AUTH_METHOD_SSH_PUBLIC_KEY = 2,
|
||||
} user_auth_method_t;
|
||||
|
||||
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;
|
||||
user_role_t role;
|
||||
user_auth_method_t method;
|
||||
size_t username_length;
|
||||
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
|
||||
} user_principal_t;
|
||||
|
||||
typedef struct {
|
||||
size_t password_length;
|
||||
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U];
|
||||
} user_database_generated_password_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
uint8_t index;
|
||||
size_t key_type_length;
|
||||
char key_type[USER_DATABASE_SSH_KEY_TYPE_CAPACITY + 1U];
|
||||
uint8_t sha256_fingerprint[USER_DATABASE_SHA256_LENGTH];
|
||||
} user_database_key_snapshot_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
uint32_t user_id;
|
||||
uint32_t auth_generation;
|
||||
user_role_t role;
|
||||
size_t username_length;
|
||||
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
|
||||
uint8_t public_key_count;
|
||||
user_database_key_snapshot_t public_keys[USER_DATABASE_MAX_SSH_KEYS_PER_USER];
|
||||
} user_database_user_snapshot_t;
|
||||
|
||||
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 Phase 8A's legacy network credential. Once bootstrapped, the two
|
||||
* credentials deliberately remain independent until the Phase 8B cutover.
|
||||
*/
|
||||
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);
|
||||
esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot);
|
||||
|
||||
esp_err_t user_database_authenticate_password(
|
||||
const uint8_t *username, size_t username_length,
|
||||
const uint8_t *password, size_t password_length,
|
||||
user_principal_t *principal, bool *authenticated);
|
||||
esp_err_t user_database_authorize_ssh_public_key(
|
||||
const uint8_t *username, size_t username_length,
|
||||
const uint8_t *key_type, size_t key_type_length,
|
||||
const uint8_t *key_blob, size_t key_blob_length,
|
||||
user_principal_t *principal, bool *authorized);
|
||||
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);
|
||||
esp_err_t user_database_create_generated(
|
||||
const uint8_t *username, size_t username_length, user_role_t role,
|
||||
user_database_generated_password_t *generated_password);
|
||||
esp_err_t user_database_delete(const uint8_t *username, size_t username_length);
|
||||
esp_err_t user_database_set_role(const uint8_t *username, size_t username_length,
|
||||
user_role_t role);
|
||||
esp_err_t user_database_set_password(const uint8_t *username, size_t username_length,
|
||||
const uint8_t *password, size_t password_length);
|
||||
esp_err_t user_database_generate_password(
|
||||
const uint8_t *username, size_t username_length,
|
||||
user_database_generated_password_t *generated_password);
|
||||
esp_err_t user_database_add_ssh_key(
|
||||
const uint8_t *username, size_t username_length,
|
||||
const uint8_t *key_type, size_t key_type_length,
|
||||
const uint8_t *key_blob, size_t key_blob_length,
|
||||
uint8_t *key_index);
|
||||
esp_err_t user_database_remove_ssh_key(const uint8_t *username,
|
||||
size_t username_length,
|
||||
uint8_t key_index);
|
||||
esp_err_t user_database_clear_ssh_keys(const uint8_t *username,
|
||||
size_t username_length);
|
||||
|
||||
bool user_database_username_valid(const uint8_t *username, size_t length);
|
||||
bool user_database_password_valid(const uint8_t *password, size_t length);
|
||||
bool user_database_key_valid(const uint8_t *key_type, size_t key_type_length,
|
||||
const uint8_t *key_blob, size_t key_blob_length);
|
||||
const char *user_role_to_string(user_role_t role);
|
||||
bool user_role_parse(const char *text, user_role_t *role);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -10,6 +10,7 @@
|
||||
#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"
|
||||
#include "web_server.h"
|
||||
@@ -223,6 +224,36 @@ 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) {
|
||||
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("Phase 8A note: this legacy HTTPS/SSH credential is separate from bootstrapped user passwords until Phase 8B.\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;
|
||||
@@ -232,6 +263,7 @@ static int rotate_credentials(void)
|
||||
return 1;
|
||||
}
|
||||
|
||||
synchronize_migrated_user(&credentials);
|
||||
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
|
||||
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
|
||||
printf("Administrative credentials rotated and persisted. Existing HTTPS and SSH credentials are now invalid.\n");
|
||||
@@ -278,6 +310,7 @@ static int reset_material(void)
|
||||
return 1;
|
||||
}
|
||||
|
||||
synchronize_migrated_user(&credentials);
|
||||
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
|
||||
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
|
||||
printf("Administrative credentials, HTTPS certificate, and HTTPS private key replaced and persisted.\n");
|
||||
|
||||
Reference in New Issue
Block a user