Implement role-aware HTTPS and SSH authentication

This commit is contained in:
2026-08-30 01:31:05 +02:00
parent cd235445c7
commit 0c058b6a8f
16 changed files with 707 additions and 331 deletions
+2 -2
View File
@@ -127,7 +127,7 @@ 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 administrative credential",
ESP_LOGI(TAG, "Using %s HTTPS identity and legacy recovery credential",
web_security_source == WEB_SECURITY_LOAD_STORED ? "stored" : "newly generated");
}
@@ -151,7 +151,7 @@ void app_main(void)
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_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));
} else {
ESP_LOGI(TAG, "Using %s user database",
+19 -14
View File
@@ -14,7 +14,7 @@
#include "secure_random.h"
#include "ssh_security.h"
#include "ssh_transport.h"
#include "web_security.h"
#include "user_database.h"
static void print_usage(void)
{
@@ -43,6 +43,13 @@ static const char *state_name(ssh_transport_session_state_t state)
}
}
static const char *auth_method_name(user_auth_method_t method)
{
return method == USER_AUTH_METHOD_PASSWORD
? "password"
: method == USER_AUTH_METHOD_SSH_PUBLIC_KEY ? "public-key" : "unknown";
}
static int print_sessions(const ssh_transport_snapshot_t *snapshot)
{
printf("SSH sessions: active=%" PRIu32 "/%u\n",
@@ -52,10 +59,18 @@ static int print_sessions(const ssh_transport_snapshot_t *snapshot)
if (!session->active) {
continue;
}
printf(" id=%" PRIu32 " slot=%u peer=%s state=%s auth=%s broker=%" PRIu32
" role=%s rx-pending=%s tx-pending=%s closing=%s\n",
printf(" id=%" PRIu32 " slot=%u peer=%s state=%s auth=%s account=%s"
" user-role=%s method=%s broker=%" PRIu32
" broker-role=%s rx-pending=%s tx-pending=%s closing=%s\n",
session->session_id, (unsigned int)index, session->peer,
state_name(session->state), session->authenticated ? "yes" : "no",
session->principal_valid ? session->username : "-",
session->principal_valid
? user_role_to_string(session->user_role)
: "-",
session->principal_valid
? auth_method_name(session->auth_method)
: "-",
session->broker_client_id,
session->broker_client_id == SESSION_BROKER_NO_CLIENT
? "unattached"
@@ -76,23 +91,13 @@ static int show_status(bool sessions_only)
return 1;
}
if (!sessions_only) {
char username[WEB_SECURITY_USERNAME_CAPACITY + 1U] = {0};
size_t username_length = 0U;
esp_err_t username_error = web_security_copy_username(
username, sizeof(username), &username_length);
printf("SSH: initialized=%s running=%s transitioning=%s port=%u last-error=%s\n",
snapshot.initialized ? "yes" : "no",
snapshot.running ? "yes" : "no",
snapshot.transitioning ? "yes" : "no",
(unsigned int)snapshot.port,
esp_err_to_name(snapshot.last_error));
if (username_error == ESP_OK) {
printf("Authentication: SSH password, username=%.*s, shared with HTTPS\n",
(int)username_length, username);
} else {
printf("Administrative credentials unavailable: %s\n",
esp_err_to_name(username_error));
}
printf("Authentication: role-based password and SSH public key via user database\n");
printf("Admission: shell/PTY only; exec, subsystem, forwarding, SCP, and SFTP disabled\n");
printf("Owner task: core=%" PRId32 " stack=%" PRIu32
" minimum-free=%" PRIu32 " bytes\n",
+264 -36
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Password-authenticated SSH stream transport with two fixed broker sessions. */
/* Role-aware SSH stream transport with two fixed broker sessions. */
#include "ssh_transport.h"
@@ -22,7 +22,7 @@
#include "secure_random.h"
#include "serial_service.h"
#include "ssh_security.h"
#include "web_security.h"
#include "user_database.h"
#include <wolfssl/wolfcrypt/memory.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssh/ssh.h>
@@ -57,6 +57,10 @@ typedef struct {
int socket_fd;
WOLFSSH *ssh;
session_broker_client_id_t broker_client_id;
user_principal_t principal;
user_principal_t pending_principal;
bool principal_valid;
bool pending_principal_valid;
bool authenticated;
bool shell_requested;
uint8_t authentication_attempts;
@@ -126,6 +130,7 @@ static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
ssh_transport_session_snapshot_t snapshot = {
.active = slot->state != SSH_TRANSPORT_SESSION_FREE,
.authenticated = slot->authenticated,
.principal_valid = slot->principal_valid,
.writer = slot->writer,
.close_requested = slot->close_requested,
.rx_pending = slot->rx_length > slot->rx_offset,
@@ -135,7 +140,15 @@ static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
.socket_fd = slot->socket_fd,
.broker_client_id = slot->broker_client_id,
.state = slot->state,
.user_role = slot->principal_valid ? slot->principal.role : USER_ROLE_USER,
.auth_method = slot->principal_valid
? slot->principal.method
: USER_AUTH_METHOD_PASSWORD,
};
if (slot->principal_valid) {
memcpy(snapshot.username, slot->principal.username,
slot->principal.username_length);
}
memcpy(snapshot.peer, slot->peer, sizeof(snapshot.peer));
taskENTER_CRITICAL(&s_lock);
@@ -223,7 +236,111 @@ static int allowed_auth_types(WOLFSSH *ssh, void *context)
{
(void)ssh;
(void)context;
return WOLFSSH_USERAUTH_PASSWORD;
return WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY;
}
static void clear_pending_principal(ssh_slot_t *slot)
{
if (slot != NULL) {
secure_wipe(&slot->pending_principal, sizeof(slot->pending_principal));
slot->pending_principal_valid = false;
}
}
static bool complete_authentication_attempt(ssh_slot_t *slot, bool failed)
{
add_counter(&s_counters.authentication_attempts, 1U);
if (failed) {
add_counter(&s_counters.authentication_failures, 1U);
}
if (slot != NULL && slot->authentication_attempts < UINT8_MAX) {
++slot->authentication_attempts;
}
if (!failed || slot == NULL ||
slot->authentication_attempts < SSH_TRANSPORT_MAX_AUTH_ATTEMPTS) {
return true;
}
slot->close_requested = true;
if (slot->socket_fd >= 0) {
(void)shutdown(slot->socket_fd, SHUT_RDWR);
}
return false;
}
static int authenticate_password(ssh_slot_t *slot,
WS_UserAuthData *authentication)
{
clear_pending_principal(slot);
if (authentication->sf.password.hasNewPassword != 0U) {
(void)complete_authentication_attempt(slot, true);
return slot->close_requested ? WOLFSSH_USERAUTH_REJECTED
: WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
user_principal_t principal;
bool authenticated = false;
esp_err_t error = user_database_authenticate_password(
authentication->username, authentication->usernameSz,
authentication->sf.password.password,
authentication->sf.password.passwordSz,
&principal, &authenticated);
if (error == ESP_OK && authenticated) {
(void)complete_authentication_attempt(slot, false);
slot->principal = principal;
slot->principal_valid = true;
slot->authenticated = true;
return WOLFSSH_USERAUTH_SUCCESS;
}
secure_wipe(&principal, sizeof(principal));
bool retry = complete_authentication_attempt(slot, true);
if (!retry) {
return WOLFSSH_USERAUTH_REJECTED;
}
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PASSWORD
: WOLFSSH_USERAUTH_FAILURE;
}
static int authenticate_public_key(ssh_slot_t *slot,
WS_UserAuthData *authentication)
{
WS_UserAuthData_PublicKey *public_key = &authentication->sf.publicKey;
clear_pending_principal(slot);
user_principal_t principal;
bool authorized = false;
esp_err_t error = ESP_OK;
if (public_key->isCert != 0U) {
memset(&principal, 0, sizeof(principal));
} else {
error = user_database_authorize_ssh_public_key(
authentication->username, authentication->usernameSz,
public_key->publicKeyType, public_key->publicKeyTypeSz,
public_key->publicKey, public_key->publicKeySz,
&principal, &authorized);
}
if (error != ESP_OK || !authorized) {
secure_wipe(&principal, sizeof(principal));
if (public_key->hasSignature == 0U) {
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PUBLICKEY
: WOLFSSH_USERAUTH_FAILURE;
}
bool retry = complete_authentication_attempt(slot, true);
if (!retry) {
return WOLFSSH_USERAUTH_REJECTED;
}
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PUBLICKEY
: WOLFSSH_USERAUTH_FAILURE;
}
if (public_key->hasSignature != 0U) {
slot->pending_principal = principal;
slot->pending_principal_valid = true;
}
secure_wipe(&principal, sizeof(principal));
return WOLFSSH_USERAUTH_SUCCESS;
}
static int authenticate_user(byte authentication_type,
@@ -231,39 +348,56 @@ static int authenticate_user(byte authentication_type,
void *context)
{
ssh_slot_t *slot = (ssh_slot_t *)context;
add_counter(&s_counters.authentication_attempts, 1U);
if (slot != NULL && slot->authentication_attempts < UINT8_MAX) {
++slot->authentication_attempts;
}
if (slot == NULL || authentication == NULL ||
authentication_type != WOLFSSH_USERAUTH_PASSWORD ||
authentication->type != WOLFSSH_USERAUTH_PASSWORD ||
authentication->sf.password.hasNewPassword != 0U) {
add_counter(&s_counters.authentication_failures, 1U);
authentication_type != authentication->type) {
clear_pending_principal(slot);
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
bool authenticated = false;
esp_err_t error = web_security_authenticate_admin(
authentication->username, authentication->usernameSz,
authentication->sf.password.password,
authentication->sf.password.passwordSz,
&authenticated);
if (error == ESP_OK && authenticated) {
slot->authenticated = true;
return WOLFSSH_USERAUTH_SUCCESS;
if (authentication_type == WOLFSSH_USERAUTH_PASSWORD) {
return authenticate_password(slot, authentication);
}
if (authentication_type == WOLFSSH_USERAUTH_PUBLICKEY) {
return authenticate_public_key(slot, authentication);
}
add_counter(&s_counters.authentication_failures, 1U);
if (slot->authentication_attempts >= SSH_TRANSPORT_MAX_AUTH_ATTEMPTS) {
slot->close_requested = true;
if (slot->socket_fd >= 0) {
(void)shutdown(slot->socket_fd, SHUT_RDWR);
}
return WOLFSSH_USERAUTH_REJECTED;
clear_pending_principal(slot);
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
static int authentication_result(byte result, WS_UserAuthData *authentication,
void *context)
{
ssh_slot_t *slot = (ssh_slot_t *)context;
if (slot == NULL || authentication == NULL ||
authentication->type != WOLFSSH_USERAUTH_PUBLICKEY ||
authentication->sf.publicKey.hasSignature == 0U) {
clear_pending_principal(slot);
return WS_ERROR;
}
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PASSWORD
: WOLFSSH_USERAUTH_FAILURE;
if (result != WOLFSSH_USERAUTH_SUCCESS) {
(void)complete_authentication_attempt(slot, true);
clear_pending_principal(slot);
return WS_SUCCESS;
}
bool current = false;
esp_err_t error = slot->pending_principal_valid
? user_database_principal_is_current(
&slot->pending_principal, &current)
: ESP_ERR_INVALID_STATE;
if (error != ESP_OK || !current) {
(void)complete_authentication_attempt(slot, true);
clear_pending_principal(slot);
return WS_ERROR;
}
(void)complete_authentication_attempt(slot, false);
slot->principal = slot->pending_principal;
slot->principal_valid = true;
slot->authenticated = true;
clear_pending_principal(slot);
return WS_SUCCESS;
}
static int accept_shell(WOLFSSH_CHANNEL *channel, void *context)
@@ -382,6 +516,7 @@ static esp_err_t create_context(void)
wolfSSH_SetIORecv(context, bounded_ssh_receive);
wolfSSH_SetUserAuth(context, authenticate_user);
wolfSSH_SetUserAuthTypes(context, allowed_auth_types);
wolfSSH_SetUserAuthResult(context, authentication_result);
(void)wolfSSH_CTX_SetChannelReqShellCb(context, accept_shell);
(void)wolfSSH_CTX_SetChannelReqExecCb(context, reject_channel_request);
(void)wolfSSH_CTX_SetChannelReqSubsysCb(context, reject_channel_request);
@@ -640,11 +775,35 @@ static void accept_connections(void)
}
wolfSSH_SetIOReadCtx(slot->ssh, slot);
wolfSSH_SetUserAuthCtx(slot->ssh, slot);
wolfSSH_SetUserAuthResultCtx(slot->ssh, slot);
wolfSSH_SetChannelReqCtx(slot->ssh, slot);
publish_slot(slot, slot_index);
}
}
static bool slot_principal_is_current(const ssh_slot_t *slot)
{
bool current = false;
return slot->principal_valid &&
user_database_principal_is_current(&slot->principal, &current) == ESP_OK &&
current;
}
static void disconnect_failed_admission(ssh_slot_t *slot)
{
if (slot->broker_client_id == SESSION_BROKER_NO_CLIENT) {
return;
}
esp_err_t error = session_broker_disconnect(slot->broker_client_id);
if (error == ESP_OK || error == ESP_ERR_NOT_FOUND) {
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
slot->writer = false;
add_counter(&s_counters.disconnections, 1U);
} else {
add_counter(&s_counters.broker_failures, 1U);
}
}
static esp_err_t connect_broker(ssh_slot_t *slot, size_t slot_index)
{
if (!serial_service_is_running()) {
@@ -657,6 +816,10 @@ static esp_err_t connect_broker(ssh_slot_t *slot, size_t slot_index)
}
}
if (!slot_principal_is_current(slot)) {
return ESP_ERR_INVALID_STATE;
}
char name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
int written = snprintf(name, sizeof(name), "ssh-%u-%" PRIu32,
(unsigned int)slot_index, slot->generation);
@@ -670,19 +833,26 @@ static esp_err_t connect_broker(ssh_slot_t *slot, size_t slot_index)
return error;
}
add_counter(&s_counters.broker_connections, 1U);
if (!slot_principal_is_current(slot)) {
disconnect_failed_admission(slot);
return ESP_ERR_INVALID_STATE;
}
add_counter(&s_counters.writer_requests, 1U);
error = session_broker_request_writer(slot->broker_client_id);
if (error == ESP_OK) {
slot->writer = true;
add_counter(&s_counters.writer_grants, 1U);
return ESP_OK;
}
if (error == ESP_ERR_INVALID_STATE) {
} else if (error == ESP_ERR_INVALID_STATE) {
slot->writer = false;
add_counter(&s_counters.writer_denials, 1U);
return ESP_OK;
} else {
return error;
}
return error;
if (!slot_principal_is_current(slot)) {
disconnect_failed_admission(slot);
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
static void process_handshake(ssh_slot_t *slot, size_t slot_index)
@@ -696,7 +866,20 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_accept(slot->ssh);
if (result == WS_SUCCESS) {
if (!slot->authenticated || !slot->shell_requested ||
bool principal_current = false;
esp_err_t principal_error = slot->principal_valid
? user_database_principal_is_current(
&slot->principal, &principal_current)
: ESP_ERR_INVALID_STATE;
if (!slot->authenticated || !slot->principal_valid) {
request_slot_close(slot, false);
return;
}
if (principal_error != ESP_OK || !principal_current) {
request_slot_close(slot, true);
return;
}
if (!slot->shell_requested ||
wolfSSH_GetSessionType(slot->ssh) != WOLFSSH_SESSION_SHELL) {
add_counter(&s_counters.request_rejections, 1U);
request_slot_close(slot, false);
@@ -709,6 +892,11 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
request_slot_close(slot, false);
return;
}
if (!slot_principal_is_current(slot)) {
disconnect_failed_admission(slot);
request_slot_close(slot, true);
return;
}
slot->state = SSH_TRANSPORT_SESSION_ACTIVE;
slot->last_reconcile_us = esp_timer_get_time();
add_counter(&s_counters.handshake_successes, 1U);
@@ -728,6 +916,16 @@ static bool reconcile_writer(ssh_slot_t *slot)
}
slot->last_reconcile_us = now;
bool principal_current = false;
esp_err_t principal_error = slot->principal_valid
? user_database_principal_is_current(
&slot->principal, &principal_current)
: ESP_ERR_INVALID_STATE;
if (principal_error != ESP_OK || !principal_current) {
request_slot_close(slot, true);
return true;
}
session_broker_client_snapshot_t snapshot;
esp_err_t error = session_broker_get_client_snapshot(
slot->broker_client_id, &snapshot);
@@ -1201,6 +1399,36 @@ esp_err_t ssh_transport_disconnect(uint32_t session_id)
return ESP_ERR_NOT_FOUND;
}
esp_err_t ssh_transport_revoke_user(const uint8_t *username,
size_t username_length)
{
if (!user_database_username_valid(username, username_length)) {
return ESP_ERR_INVALID_ARG;
}
bool found = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
const ssh_transport_session_snapshot_t *session =
&s_session_snapshots[index];
if (session->active && session->principal_valid &&
strlen(session->username) == username_length &&
memcmp(session->username, username, username_length) == 0) {
s_external_close_id[index] = session->session_id;
found = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (found) {
notify_task();
}
return ESP_OK;
}
esp_err_t ssh_transport_revoke_sessions(void)
{
taskENTER_CRITICAL(&s_lock);
+8 -1
View File
@@ -9,6 +9,7 @@
#include "esp_err.h"
#include "session_broker.h"
#include "user_database.h"
#ifdef __cplusplus
extern "C" {
@@ -56,6 +57,7 @@ typedef struct {
typedef struct {
bool active;
bool authenticated;
bool principal_valid;
bool writer;
bool close_requested;
bool rx_pending;
@@ -65,6 +67,9 @@ typedef struct {
int socket_fd;
session_broker_client_id_t broker_client_id;
ssh_transport_session_state_t state;
user_role_t user_role;
user_auth_method_t auth_method;
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
char peer[48];
} ssh_transport_session_snapshot_t;
@@ -93,8 +98,10 @@ esp_err_t ssh_transport_replace_host_key(bool reset);
esp_err_t ssh_transport_get_snapshot(ssh_transport_snapshot_t *snapshot);
esp_err_t ssh_transport_clear_counters(void);
/* Close one transport session or all authenticated/handshaking sessions. */
/* Close one session, one account's sessions, or every transport session. */
esp_err_t ssh_transport_disconnect(uint32_t session_id);
esp_err_t ssh_transport_revoke_user(const uint8_t *username,
size_t username_length);
esp_err_t ssh_transport_revoke_sessions(void);
#ifdef __cplusplus
+29 -3
View File
@@ -11,8 +11,10 @@
#include "esp_console.h"
#include "mbedtls/base64.h"
#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
@@ -32,6 +34,22 @@ static void print_usage(void)
printf(" user key clear <username> --force\n");
}
static void revoke_user_network_sessions(const char *username)
{
size_t username_length = strlen(username);
esp_err_t web_error = web_serial_transport_revoke_user(
(const uint8_t *)username, username_length);
esp_err_t ssh_error = ssh_transport_revoke_user(
(const uint8_t *)username, username_length);
if (web_error != ESP_OK && web_error != ESP_ERR_INVALID_STATE) {
printf("Warning: WebSocket revocation failed: %s\n",
esp_err_to_name(web_error));
}
if (ssh_error != ESP_OK && ssh_error != ESP_ERR_INVALID_STATE) {
printf("Warning: SSH revocation failed: %s\n", esp_err_to_name(ssh_error));
}
}
static void print_fingerprint(const uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH])
{
uint8_t encoded[48] = {0};
@@ -191,7 +209,8 @@ static int bootstrap(bool generated)
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");
revoke_user_network_sessions("admin");
printf("Administrator account bootstrapped. Role-aware HTTPS and SSH authentication is active.\n");
return 0;
}
@@ -225,6 +244,7 @@ static int add_user(const char *username, const char *role_text, bool generated)
printf("Could not add user: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(username);
printf("User '%s' added with role %s.\n", username, user_role_to_string(role));
return 0;
}
@@ -254,7 +274,8 @@ static int change_password(const char *username, bool generated)
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");
revoke_user_network_sessions(username);
printf("Password changed; affected network sessions are now stale and will be revoked.\n");
return 0;
}
@@ -338,7 +359,8 @@ static int add_key(const char *username)
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",
revoke_user_network_sessions(username);
printf("SSH public key added at index %u. Public-key login is active.\n",
(unsigned int)key_index);
return 0;
}
@@ -380,6 +402,7 @@ static int command_user(int argc, char **argv)
esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[2]);
printf("User '%s' deleted.\n", argv[2]);
return 0;
}
@@ -398,6 +421,7 @@ static int command_user(int argc, char **argv)
esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[2]);
printf("User '%s' role changed to %s.\n", argv[2], user_role_to_string(role));
return 0;
}
@@ -426,6 +450,7 @@ static int command_user(int argc, char **argv)
printf("Could not delete SSH key: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[3]);
printf("SSH key %u deleted for '%s'.\n", (unsigned int)index, argv[3]);
return 0;
}
@@ -437,6 +462,7 @@ static int command_user(int argc, char **argv)
printf("Could not clear SSH keys: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[3]);
printf("SSH keys cleared for '%s'.\n", argv[3]);
return 0;
}
+2 -2
View File
@@ -94,8 +94,8 @@ 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.
* 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);
+30 -36
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 HTTPS lifecycle, shared credentials, certificate, and recovery commands. */
/* UART0 HTTPS lifecycle, legacy recovery credential, and certificate commands. */
#include "web_console.h"
@@ -43,22 +43,20 @@ static int show_status(void)
return 1;
}
char username[WEB_SECURITY_USERNAME_CAPACITY + 1U] = {0};
size_t username_length = 0U;
esp_err_t security_error = web_security_copy_username(
username, sizeof(username), &username_length);
user_database_snapshot_t users;
esp_err_t users_error = user_database_get_snapshot(&users);
printf("HTTPS: initialized=%s running=%s transitioning=%s port=%u last-error=%s\n",
snapshot.initialized ? "yes" : "no",
snapshot.running ? "yes" : "no",
snapshot.transitioning ? "yes" : "no",
(unsigned int)snapshot.port,
esp_err_to_name(snapshot.last_error));
if (security_error == ESP_OK) {
printf("Authentication: HTTP Basic over TLS, username=%.*s, shared with SSH\n",
(int)username_length, username);
if (users_error == ESP_OK) {
printf("Authentication: HTTP Basic over TLS via user database, users=%u admins=%u\n",
(unsigned int)users.user_count, (unsigned int)users.admin_count);
} else {
printf("Authentication material unavailable: %s; use 'web reset --force' to replace it.\n",
esp_err_to_name(security_error));
printf("Authentication database unavailable: %s; use 'user recover --force'.\n",
esp_err_to_name(users_error));
}
printf("Endpoints: GET /, GET /api/status, POST /api/ws-ticket, WSS /ws/serial\n");
@@ -81,11 +79,20 @@ static int show_status(void)
if (!session->active) {
continue;
}
printf(" slot=%u fd=%d generation=%" PRIu32 " broker=%" PRIu32
" role=%s tx-pending=%s closing=%s\n",
printf(" slot=%u fd=%d generation=%" PRIu32 " account=%s user-role=%s"
" method=%s broker=%" PRIu32
" broker-role=%s tx-pending=%s closing=%s\n",
(unsigned int)index,
session->socket_fd,
session->generation,
session->principal_valid ? session->username : "-",
session->principal_valid
? user_role_to_string(session->user_role)
: "-",
session->principal_valid &&
session->auth_method == USER_AUTH_METHOD_PASSWORD
? "password"
: "unknown",
session->broker_client_id,
session->writer ? "writer" : "observer",
session->tx_pending ? "yes" : "no",
@@ -169,7 +176,8 @@ static int show_credentials(void)
credentials.username);
printf("Password: %.*s\n", (int)credentials.password_length,
credentials.password);
printf("These credentials protect HTTPS and SSH. Keep them private.\n");
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;
}
@@ -241,6 +249,12 @@ static void synchronize_migrated_user(
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;
}
@@ -248,7 +262,7 @@ static void synchronize_migrated_user(
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");
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");
}
@@ -264,17 +278,7 @@ static int rotate_credentials(void)
}
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");
if (web_revoke_error != ESP_OK && web_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing WebSocket sessions could not be revoked: %s\n",
esp_err_to_name(web_revoke_error));
}
if (ssh_revoke_error != ESP_OK && ssh_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing SSH sessions could not be revoked: %s\n",
esp_err_to_name(ssh_revoke_error));
}
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);
@@ -311,17 +315,7 @@ static int reset_material(void)
}
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");
if (web_revoke_error != ESP_OK && web_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing WebSocket sessions could not be revoked: %s\n",
esp_err_to_name(web_revoke_error));
}
if (ssh_revoke_error != ESP_OK && ssh_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing SSH sessions could not be revoked: %s\n",
esp_err_to_name(ssh_revoke_error));
}
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);
+1 -112
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Canonical NVS storage for HTTPS identity and shared admin credentials. */
/* Canonical NVS storage for HTTPS identity and legacy recovery credentials. */
#include "web_security.h"
@@ -699,117 +699,6 @@ esp_err_t web_security_copy_tls_material(
return error;
}
esp_err_t web_security_copy_username(char *output, size_t capacity,
size_t *output_length)
{
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) {
if (output_length == NULL || (output == NULL && capacity != 0U)) {
error = ESP_ERR_INVALID_ARG;
} else {
*output_length = s_material.username_length;
if (output == NULL) {
error = capacity == 0U ? ESP_OK : ESP_ERR_INVALID_ARG;
} else if (capacity <= s_material.username_length) {
error = ESP_ERR_INVALID_SIZE;
} else {
memcpy(output, s_material.username, s_material.username_length);
output[s_material.username_length] = '\0';
error = ESP_OK;
}
}
}
xSemaphoreGive(s_security_mutex);
return error;
}
static esp_err_t credential_digest(const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length,
uint8_t digest[WEB_SECURITY_SHA256_LENGTH])
{
uint8_t canonical[4U + WEB_SECURITY_USERNAME_CAPACITY +
WEB_SECURITY_PASSWORD_CAPACITY] = {0};
size_t offset = 0U;
canonical[offset++] = (uint8_t)(username_length >> 8U);
canonical[offset++] = (uint8_t)username_length;
if (username_length > 0U) {
memcpy(canonical + offset, username, username_length);
offset += username_length;
}
canonical[offset++] = (uint8_t)(password_length >> 8U);
canonical[offset++] = (uint8_t)password_length;
if (password_length > 0U) {
memcpy(canonical + offset, password, password_length);
offset += password_length;
}
int result = mbedtls_sha256(canonical, offset, digest, 0);
secure_wipe(canonical, sizeof(canonical));
return result == 0 ? ESP_OK : ESP_FAIL;
}
esp_err_t web_security_authenticate_admin(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated)
{
if (authenticated == NULL ||
(username == NULL && username_length != 0U) ||
(password == NULL && password_length != 0U)) {
return ESP_ERR_INVALID_ARG;
}
*authenticated = false;
if (s_security_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
if (username_length > WEB_SECURITY_USERNAME_CAPACITY ||
password_length > WEB_SECURITY_PASSWORD_CAPACITY) {
return ESP_OK;
}
uint8_t supplied_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
uint8_t expected_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
esp_err_t error = ESP_ERR_INVALID_STATE;
if (s_material_ready) {
error = credential_digest(username, username_length,
password, password_length,
supplied_digest);
if (error == ESP_OK) {
error = credential_digest(s_material.username,
s_material.username_length,
s_material.password,
s_material.password_length,
expected_digest);
}
if (error == ESP_OK) {
*authenticated = constant_time_equal(supplied_digest,
expected_digest,
sizeof(expected_digest));
}
}
xSemaphoreGive(s_security_mutex);
secure_wipe(supplied_digest, sizeof(supplied_digest));
secure_wipe(expected_digest, sizeof(expected_digest));
return error;
}
esp_err_t web_security_authenticate_basic(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated)
{
return web_security_authenticate_admin(username, username_length,
password, password_length,
authenticated);
}
static void copy_credentials_locked(web_security_credentials_t *credentials,
const web_security_blob_t *blob)
+1 -17
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Persistent HTTPS identity and shared network-administration credentials. */
/* Persistent HTTPS identity and legacy migration/recovery credentials. */
#pragma once
@@ -72,22 +72,6 @@ esp_err_t web_security_copy_tls_material(
size_t *certificate_length,
uint8_t *private_key, size_t private_key_capacity,
size_t *private_key_length);
esp_err_t web_security_copy_username(char *output, size_t capacity,
size_t *output_length);
/* Protocol-neutral authentication for the shared HTTPS and SSH administrator. */
esp_err_t web_security_authenticate_admin(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated);
/* Compatibility name for decoded HTTP Basic components. */
esp_err_t web_security_authenticate_basic(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated);
/* Explicit secret-bearing API intended for a physically attached UART CLI. */
esp_err_t web_security_show_credentials(web_security_credentials_t *credentials);
+241 -68
View File
@@ -14,7 +14,6 @@
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
#include "web_security.h"
#if !defined(CONFIG_HTTPD_WS_SUPPORT) || !CONFIG_HTTPD_WS_SUPPORT
#error "web_serial_transport requires CONFIG_HTTPD_WS_SUPPORT"
@@ -30,6 +29,7 @@
#define WEB_SERIAL_TASK_PRIORITY 4U
#define WEB_SERIAL_ACTIVE_BURST_LOOPS 8U
#define WEB_SERIAL_POLL_MS 5U
#define WEB_SERIAL_CURRENTNESS_INTERVAL_US 250000LL
#define WEB_SERIAL_DETACH_TIMEOUT_US 1000000LL
_Static_assert(WEB_SERIAL_TRANSPORT_TICKET_LENGTH ==
@@ -48,7 +48,7 @@ typedef enum {
typedef struct {
uint8_t digest[WEB_SERIAL_SHA256_BYTES];
int64_t expires_at_us;
uint32_t material_generation;
user_principal_t principal;
bool active;
} web_serial_ticket_t;
@@ -67,6 +67,8 @@ typedef struct web_serial_slot {
int socket_fd;
uint32_t generation;
session_broker_client_id_t broker_client_id;
user_principal_t principal;
int64_t next_currentness_check_us;
bool writer;
bool hello_pending;
bool work_pending;
@@ -142,8 +144,8 @@ static bool slot_pointer_valid(const web_serial_slot_t *slot)
static void clear_ticket_locked(web_serial_ticket_t *ticket)
{
secure_wipe(ticket->digest, sizeof(ticket->digest));
secure_wipe(&ticket->principal, sizeof(ticket->principal));
ticket->expires_at_us = 0;
ticket->material_generation = 0U;
ticket->active = false;
}
@@ -154,20 +156,13 @@ static void clear_all_tickets_locked(void)
}
}
static void purge_tickets_locked(int64_t now_us, uint32_t material_generation,
bool check_generation)
static void purge_tickets_locked(int64_t now_us)
{
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (!ticket->active) {
continue;
}
if (ticket->expires_at_us <= now_us) {
if (ticket->active && ticket->expires_at_us <= now_us) {
clear_ticket_locked(ticket);
++s_counters.tickets_expired;
} else if (check_generation &&
ticket->material_generation != material_generation) {
clear_ticket_locked(ticket);
}
}
}
@@ -209,16 +204,6 @@ static bool base64url_character(char value)
(value >= '0' && value <= '9') || value == '-' || value == '_';
}
static esp_err_t current_material_generation(uint32_t *generation)
{
web_security_certificate_metadata_t metadata;
esp_err_t result = web_security_get_certificate_metadata(&metadata);
if (result == ESP_OK) {
*generation = metadata.material_generation;
}
secure_wipe(&metadata, sizeof(metadata));
return result;
}
static esp_err_t sha256_ticket(const char *ticket,
uint8_t digest[WEB_SERIAL_SHA256_BYTES])
@@ -295,16 +280,16 @@ static esp_err_t validate_origin(httpd_req_t *request)
return matches ? ESP_OK : ESP_ERR_INVALID_ARG;
}
static esp_err_t consume_ticket(const char *ticket, bool *consumed)
static esp_err_t consume_ticket(const char *ticket,
user_principal_t *principal, bool *consumed)
{
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
user_principal_t candidate = {0};
bool ticket_found = false;
*consumed = false;
memset(principal, 0, sizeof(*principal));
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = sha256_ticket(ticket, digest);
}
esp_err_t result = sha256_ticket(ticket, digest);
if (result != ESP_OK) {
secure_wipe(digest, sizeof(digest));
add_counter(&s_counters.tickets_rejected, 1U);
@@ -326,36 +311,45 @@ static esp_err_t consume_ticket(const char *ticket, bool *consumed)
}
}
purge_tickets_locked(now_us, generation, true);
purge_tickets_locked(now_us);
if (matching_count == 1U &&
matching_index < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[matching_index];
if (entry->active && entry->expires_at_us > now_us &&
entry->material_generation == generation) {
if (entry->active && entry->expires_at_us > now_us) {
candidate = entry->principal;
clear_ticket_locked(entry);
++s_counters.tickets_consumed;
*consumed = true;
ticket_found = true;
}
}
if (!*consumed) {
if (!ticket_found && matching_count > 1U) {
/* A defensive duplicate can never become a repeatedly usable ticket. */
if (matching_count > 1U) {
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (entry->active && constant_time_equal(
entry->digest, digest,
sizeof(digest))) {
clear_ticket_locked(entry);
}
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (entry->active && constant_time_equal(
entry->digest, digest, sizeof(digest))) {
clear_ticket_locked(entry);
}
}
++s_counters.tickets_rejected;
}
taskEXIT_CRITICAL(&s_lock);
if (ticket_found) {
bool current = false;
result = user_database_principal_is_current(&candidate, &current);
if (result == ESP_OK && current) {
*principal = candidate;
*consumed = true;
add_counter(&s_counters.tickets_consumed, 1U);
}
}
if (!*consumed) {
add_counter(&s_counters.tickets_rejected, 1U);
}
secure_wipe(&candidate, sizeof(candidate));
secure_wipe(digest, sizeof(digest));
return ESP_OK;
return result;
}
static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
@@ -376,6 +370,8 @@ static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
slot->server = server;
slot->socket_fd = socket_fd;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -399,6 +395,8 @@ static void make_slot_free_locked(web_serial_slot_t *slot)
slot->server = NULL;
slot->socket_fd = -1;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -481,6 +479,8 @@ static void web_serial_session_free(void *context)
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
@@ -517,6 +517,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
uint32_t slot_generation = 0U;
web_serial_slot_t *slot = NULL;
user_principal_t principal = {0};
bool consumed = false;
esp_err_t result;
@@ -534,12 +535,25 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
result = consume_ticket(ticket, &consumed);
result = consume_ticket(ticket, &principal, &consumed);
if (result != ESP_OK || !consumed) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
taskENTER_CRITICAL(&s_lock);
bool principal_staged = slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
!slot->close_requested;
if (principal_staged) {
slot->principal = principal;
}
taskEXIT_CRITICAL(&s_lock);
if (!principal_staged) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
if (!serial_service_is_running()) {
result = serial_service_start();
@@ -556,6 +570,14 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
}
}
bool principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
size_t slot_index = (size_t)(slot - s_slots);
char client_name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
int written = snprintf(client_name, sizeof(client_name),
@@ -579,6 +601,14 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
add_counter(&s_counters.writer_requests, 1U);
result = session_broker_request_writer(client_id);
bool writer = result == ESP_OK;
@@ -594,13 +624,25 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
bool activated = false;
int64_t next_currentness_check_us =
monotonic_time_us() + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
slot->generation == slot_generation && !slot->close_requested &&
s_server == request->handle) {
slot->state = WEB_SERIAL_SLOT_ACTIVE;
slot->broker_client_id = client_id;
slot->principal = principal;
slot->next_currentness_check_us = next_currentness_check_us;
slot->writer = writer;
slot->hello_pending = true;
++s_counters.connections;
@@ -622,13 +664,15 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
result = ESP_OK;
cleanup:
secure_wipe(&principal, sizeof(principal));
secure_wipe(ticket, sizeof(ticket));
return result;
}
static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slot_out,
uint32_t *generation,
session_broker_client_id_t *client_id)
session_broker_client_id_t *client_id,
user_principal_t *principal)
{
web_serial_slot_t *slot = request->sess_ctx;
int socket_fd = httpd_req_to_sockfd(request);
@@ -647,6 +691,7 @@ static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slo
*slot_out = slot;
*generation = slot->generation;
*client_id = slot->broker_client_id;
*principal = slot->principal;
}
taskEXIT_CRITICAL(&s_lock);
return valid;
@@ -757,7 +802,9 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
web_serial_slot_t *slot = NULL;
uint32_t generation = 0U;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
if (!capture_active_session(request, &slot, &generation, &client_id)) {
user_principal_t principal = {0};
if (!capture_active_session(request, &slot, &generation, &client_id,
&principal)) {
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
@@ -765,27 +812,52 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
int socket_fd = httpd_req_to_sockfd(request);
if (httpd_ws_get_fd_info(request->handle, socket_fd) !=
HTTPD_WS_CLIENT_WEBSOCKET) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, 0U);
}
httpd_ws_frame_t frame = {0};
esp_err_t result = httpd_ws_recv_frame(request, &frame, 0U);
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
if (!frame.final || frame.type == HTTPD_WS_TYPE_CONTINUE ||
frame.len > WEB_SERIAL_TRANSPORT_MAX_RX_PAYLOAD ||
(frame.type != HTTPD_WS_TYPE_BINARY &&
frame.type != HTTPD_WS_TYPE_TEXT)) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
frame.payload = slot->rx_data;
result = httpd_ws_recv_frame(request, &frame, sizeof(slot->rx_data));
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
bool current = false;
esp_err_t currentness_result =
user_database_principal_is_current(&principal, &current);
secure_wipe(&principal, sizeof(principal));
if (currentness_result != ESP_OK || !current) {
request_handler_close(slot, generation);
return ESP_FAIL;
}
taskENTER_CRITICAL(&s_lock);
bool still_active = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->close_requested &&
slot->generation == generation &&
slot->broker_client_id == client_id &&
slot->server == request->handle &&
slot->server == s_server;
taskEXIT_CRITICAL(&s_lock);
if (!still_active) {
return ESP_FAIL;
}
if (frame.type == HTTPD_WS_TYPE_BINARY) {
return process_binary_frame(slot, generation, client_id,
slot->rx_data, frame.len);
@@ -817,19 +889,31 @@ static void web_serial_send_work(void *argument)
httpd_ws_type_t type;
size_t length;
bool valid;
bool retired = false;
taskENTER_CRITICAL(&s_lock);
valid = slot->work_pending && work == &slot->work &&
work->generation == slot->generation &&
work->server == slot->server &&
work->socket_fd == slot->socket_fd;
bool owned_work = slot->work_pending && work == &slot->work &&
work->generation == slot->generation &&
work->server == slot->server &&
work->socket_fd == slot->socket_fd;
valid = owned_work && slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->close_requested;
server = work->server;
socket_fd = work->socket_fd;
generation = work->generation;
type = slot->tx_type;
length = slot->tx_length;
if (owned_work && !valid) {
slot->work_pending = false;
slot->tx_length = 0U;
finish_closing_slot_locked(slot);
retired = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!valid) {
if (retired) {
notify_transport_task();
}
return;
}
@@ -1113,6 +1197,8 @@ static void process_close_request(web_serial_slot_t *slot)
++s_counters.queue_failures;
if (session_gone) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -1164,6 +1250,42 @@ static void process_broker_disconnect(web_serial_slot_t *slot)
taskEXIT_CRITICAL(&s_lock);
}
static void process_principal_currentness(web_serial_slot_t *slot)
{
user_principal_t principal = {0};
uint32_t generation = 0U;
bool check = false;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE && !slot->close_requested &&
slot->next_currentness_check_us <= now_us) {
generation = slot->generation;
principal = slot->principal;
slot->next_currentness_check_us =
now_us + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
check = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!check) {
return;
}
bool current = false;
esp_err_t result = user_database_principal_is_current(&principal, &current);
secure_wipe(&principal, sizeof(principal));
if (result == ESP_OK && current) {
return;
}
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
}
static void process_active_output(web_serial_slot_t *slot)
{
uint32_t generation;
@@ -1221,6 +1343,7 @@ static void transport_task(void *context)
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
web_serial_slot_t *slot = &s_slots[index];
process_principal_currentness(slot);
process_close_request(slot);
process_broker_disconnect(slot);
process_active_output(slot);
@@ -1332,6 +1455,8 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
slot->tx_length = 0U;
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
@@ -1398,20 +1523,27 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
return result;
}
esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
char *ticket, size_t capacity)
{
if (ticket == NULL || capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
if (principal == NULL || ticket == NULL ||
capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
return ESP_ERR_INVALID_ARG;
}
ticket[0] = '\0';
bool current = false;
esp_err_t result = user_database_principal_is_current(principal, &current);
if (result != ESP_OK) {
return result;
}
if (!current) {
return ESP_ERR_INVALID_STATE;
}
uint8_t random_bytes[WEB_SERIAL_RANDOM_BYTES] = {0};
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = secure_random_fill(random_bytes, sizeof(random_bytes));
}
result = secure_random_fill(random_bytes, sizeof(random_bytes));
if (result == ESP_OK) {
encode_base64url_24(random_bytes, ticket);
result = sha256_ticket(ticket, digest);
@@ -1425,7 +1557,7 @@ esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
bool stored = false;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && s_server != NULL) {
purge_tickets_locked(now_us, generation, true);
purge_tickets_locked(now_us);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
@@ -1447,7 +1579,7 @@ esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
entry->expires_at_us =
now_us + (int64_t)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS *
1000000LL;
entry->material_generation = generation;
entry->principal = *principal;
entry->active = true;
++s_counters.tickets_issued;
stored = true;
@@ -1467,9 +1599,9 @@ cleanup:
}
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request)
httpd_req_t *request, const user_principal_t *principal)
{
if (request == NULL) {
if (request == NULL || principal == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (request->method != HTTP_POST || request->content_len != 0U ||
@@ -1487,7 +1619,8 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
char response[WEB_SERIAL_TICKET_RESPONSE_CAPACITY];
esp_err_t result = web_serial_transport_mint_ticket(ticket, sizeof(ticket));
esp_err_t result = web_serial_transport_mint_ticket(
principal, ticket, sizeof(ticket));
if (result != ESP_OK) {
secure_wipe(ticket, sizeof(ticket));
return result;
@@ -1573,8 +1706,6 @@ esp_err_t web_serial_transport_get_snapshot(
return ESP_ERR_INVALID_ARG;
}
uint32_t generation = 0U;
bool have_generation = current_material_generation(&generation) == ESP_OK;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
@@ -1582,7 +1713,7 @@ esp_err_t web_serial_transport_get_snapshot(
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
purge_tickets_locked(now_us, generation, have_generation);
purge_tickets_locked(now_us);
memset(snapshot, 0, sizeof(*snapshot));
snapshot->initialized = true;
snapshot->server_attached = s_server != NULL;
@@ -1602,12 +1733,17 @@ esp_err_t web_serial_transport_get_snapshot(
continue;
}
session->active = true;
session->principal_valid = true;
session->writer = slot->writer;
session->tx_pending = slot->work_pending;
session->close_requested = slot->close_requested;
session->socket_fd = slot->socket_fd;
session->generation = slot->generation;
session->broker_client_id = slot->broker_client_id;
session->user_role = slot->principal.role;
session->auth_method = slot->principal.method;
memcpy(session->username, slot->principal.username,
slot->principal.username_length);
++snapshot->active_sessions;
}
taskEXIT_CRITICAL(&s_lock);
@@ -1626,6 +1762,43 @@ esp_err_t web_serial_transport_clear_counters(void)
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
size_t username_length)
{
if (!user_database_username_valid(username, username_length)) {
return ESP_ERR_INVALID_ARG;
}
bool notify = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (ticket->active && ticket->principal.username_length == username_length &&
memcmp(ticket->principal.username, username, username_length) == 0) {
clear_ticket_locked(ticket);
}
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
web_serial_slot_t *slot = &s_slots[index];
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
slot->state == WEB_SERIAL_SLOT_ACTIVE) &&
slot->principal.username_length == username_length &&
memcmp(slot->principal.username, username, username_length) == 0) {
slot->close_requested = true;
notify = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (notify) {
notify_transport_task();
}
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_sessions(void)
{
taskENTER_CRITICAL(&s_lock);
+16 -8
View File
@@ -10,6 +10,7 @@
#include "esp_err.h"
#include "esp_http_server.h"
#include "session_broker.h"
#include "user_database.h"
#ifdef __cplusplus
extern "C" {
@@ -63,12 +64,16 @@ typedef struct {
typedef struct {
bool active;
bool principal_valid;
bool writer;
bool tx_pending;
bool close_requested;
int socket_fd;
uint32_t generation;
session_broker_client_id_t broker_client_id;
user_role_t user_role;
user_auth_method_t auth_method;
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
} web_serial_transport_session_snapshot_t;
typedef struct {
@@ -93,19 +98,20 @@ esp_err_t web_serial_transport_attach_server(httpd_handle_t server);
esp_err_t web_serial_transport_detach_server(httpd_handle_t server);
/*
* Mint a one-time bearer ticket for an already-authenticated caller. The output
* is exactly 32 Base64URL characters plus a terminator and expires after 30
* monotonic seconds. Never log or persist the returned value.
* Mint a one-time bearer ticket bound to a current authenticated principal. The
* principal is copied; the output is exactly 32 Base64URL characters plus a
* terminator and expires after 30 monotonic seconds. Never log or persist it.
*/
esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity);
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
char *ticket, size_t capacity);
/*
* Convenience POST response helper for /api/ws-ticket. Authentication is
* intentionally outside this module: call this only after Basic authentication
* has already succeeded. Register it as HTTP_POST, not as a public handler.
* intentionally outside this module: pass the principal returned by successful
* Basic authentication. Register it as HTTP_POST, not as a public handler.
*/
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request);
httpd_req_t *request, const user_principal_t *principal);
/*
* Handler for /ws/serial. Register as HTTP_GET with is_websocket=true and
@@ -120,7 +126,9 @@ esp_err_t web_serial_transport_get_snapshot(
/* Clearing counters does not alter tickets, sessions, ownership, or queued data. */
esp_err_t web_serial_transport_clear_counters(void);
/* Invalidate outstanding tickets and close authenticated web serial sessions. */
/* Invalidate tickets/sessions for one account, or all authenticated sessions. */
esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
size_t username_length);
esp_err_t web_serial_transport_revoke_sessions(void);
#ifdef __cplusplus
+35 -12
View File
@@ -20,14 +20,19 @@
#include "serial_service.h"
#include "session_broker.h"
#include "usb_cdc_transport.h"
#include "user_database.h"
#include "web_security.h"
#include "web_serial_transport.h"
#include "web_ui.h"
#include "wifi_manager.h"
#define WEB_SERVER_PORT 443U
#define WEB_SERVER_MAX_AUTHORIZATION 128U
#define WEB_SERVER_MAX_BASIC_DECODED 64U
#define WEB_SERVER_MAX_BASIC_DECODED \
(USER_DATABASE_USERNAME_CAPACITY + 1U + USER_DATABASE_PASSWORD_CAPACITY)
#define WEB_SERVER_MAX_BASIC_ENCODED \
(((WEB_SERVER_MAX_BASIC_DECODED + 2U) / 3U) * 4U)
#define WEB_SERVER_MAX_AUTHORIZATION \
((sizeof("Basic ") - 1U) + WEB_SERVER_MAX_BASIC_ENCODED + 1U)
#define WEB_SERVER_STATUS_JSON_CAPACITY 3072U
static SemaphoreHandle_t s_server_mutex;
@@ -101,12 +106,15 @@ static esp_err_t send_authentication_required(httpd_req_t *request)
return send_plain_error(request, "401 Unauthorized", "Authentication required.\n");
}
static esp_err_t authenticate_request(httpd_req_t *request, bool *authenticated)
static esp_err_t authenticate_request(httpd_req_t *request,
user_principal_t *principal,
bool *authenticated)
{
char authorization[WEB_SERVER_MAX_AUTHORIZATION] = {0};
uint8_t decoded[WEB_SERVER_MAX_BASIC_DECODED] = {0};
size_t decoded_length = 0U;
esp_err_t result = ESP_OK;
memset(principal, 0, sizeof(*principal));
*authenticated = false;
increment_counter(&s_counters.requests);
@@ -133,28 +141,32 @@ static esp_err_t authenticate_request(httpd_req_t *request, bool *authenticated)
}
size_t username_length = (size_t)(separator - decoded);
size_t password_length = decoded_length - username_length - 1U;
result = web_security_authenticate_basic(decoded, username_length,
separator + 1U, password_length,
authenticated);
result = user_database_authenticate_password(
decoded, username_length, separator + 1U, password_length,
principal, authenticated);
cleanup:
secure_wipe(authorization, sizeof(authorization));
secure_wipe(decoded, sizeof(decoded));
if (result != ESP_OK) {
memset(principal, 0, sizeof(*principal));
return result;
}
if (*authenticated) {
increment_counter(&s_counters.authenticated_requests);
} else {
memset(principal, 0, sizeof(*principal));
increment_counter(&s_counters.authentication_failures);
}
return ESP_OK;
}
static esp_err_t authorize_or_respond(httpd_req_t *request, bool *authorized)
static esp_err_t authorize_or_respond(httpd_req_t *request,
user_principal_t *principal,
bool *authorized)
{
*authorized = false;
esp_err_t error = authenticate_request(request, authorized);
esp_err_t error = authenticate_request(request, principal, authorized);
if (error != ESP_OK) {
*authorized = false;
return send_plain_error(request, "503 Service Unavailable",
@@ -167,14 +179,17 @@ static esp_err_t send_authenticated_ui(httpd_req_t *request,
web_ui_resource_t resource,
uint64_t *counter)
{
user_principal_t principal = {0};
bool authorized = false;
esp_err_t error = authorize_or_respond(request, &authorized);
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
if (error != ESP_OK || !authorized) {
secure_wipe(&principal, sizeof(principal));
return error;
}
increment_counter(counter);
error = web_ui_send_response(request, resource);
secure_wipe(&principal, sizeof(principal));
if (error != ESP_OK) {
increment_counter(&s_counters.response_errors);
}
@@ -195,18 +210,23 @@ static esp_err_t asset_handler(httpd_req_t *request)
static esp_err_t ticket_handler(httpd_req_t *request)
{
user_principal_t principal = {0};
bool authorized = false;
esp_err_t error = authorize_or_respond(request, &authorized);
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
if (error != ESP_OK || !authorized) {
secure_wipe(&principal, sizeof(principal));
return error;
}
increment_counter(&s_counters.ticket_requests);
if (request->content_len != 0U) {
secure_wipe(&principal, sizeof(principal));
return send_plain_error(request, "400 Bad Request",
"Ticket requests must have an empty body.\n");
}
error = web_serial_transport_handle_authenticated_ticket_request(request);
error = web_serial_transport_handle_authenticated_ticket_request(
request, &principal);
secure_wipe(&principal, sizeof(principal));
if (error == ESP_OK) {
return ESP_OK;
}
@@ -254,11 +274,14 @@ static void format_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LEN
static esp_err_t status_handler(httpd_req_t *request)
{
user_principal_t principal = {0};
bool authorized = false;
esp_err_t error = authorize_or_respond(request, &authorized);
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
if (error != ESP_OK || !authorized) {
secure_wipe(&principal, sizeof(principal));
return error;
}
secure_wipe(&principal, sizeof(principal));
increment_counter(&s_counters.status_requests);
wifi_manager_snapshot_t wifi = {0};