Implement role-aware HTTPS and SSH authentication
This commit is contained in:
+2
-2
@@ -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
@@ -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
@@ -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, ¤t)
|
||||
: 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, ¤t) == 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, ¤t);
|
||||
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, ¤t);
|
||||
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, ¤t);
|
||||
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, ¤t);
|
||||
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);
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user