Add Serialized SSH Administrative Console
This commit is contained in:
@@ -21,6 +21,8 @@ idf_component_register(
|
||||
"session_broker.c"
|
||||
"session_console.c"
|
||||
"ssh_security.c"
|
||||
"admin_command_gate.c"
|
||||
"admin_ssh_console.c"
|
||||
"ssh_transport.c"
|
||||
"ssh_console.c"
|
||||
"usb_cdc_transport.c"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Shared recursive gate for administrative command execution origins. */
|
||||
|
||||
#include "admin_command_gate.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static SemaphoreHandle_t s_gate;
|
||||
|
||||
esp_err_t admin_command_gate_take(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
SemaphoreHandle_t gate = s_gate;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (gate == NULL) {
|
||||
SemaphoreHandle_t candidate = xSemaphoreCreateRecursiveMutex();
|
||||
if (candidate == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
if (s_gate == NULL) {
|
||||
s_gate = candidate;
|
||||
candidate = NULL;
|
||||
}
|
||||
gate = s_gate;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (candidate != NULL) {
|
||||
vSemaphoreDelete(candidate);
|
||||
}
|
||||
}
|
||||
return xSemaphoreTakeRecursive(gate, portMAX_DELAY) == pdTRUE ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
void admin_command_gate_give(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
SemaphoreHandle_t gate = s_gate;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (gate != NULL) {
|
||||
(void)xSemaphoreGiveRecursive(gate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Serializes trusted UART0 and authenticated SSH administrative mutations. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
esp_err_t admin_command_gate_take(void);
|
||||
void admin_command_gate_give(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,628 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Serialized, bounded administrative SSH command worker. */
|
||||
|
||||
#include "admin_ssh_console.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_console.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "linenoise/linenoise.h"
|
||||
#include "secure_random.h"
|
||||
#include "user_database.h"
|
||||
|
||||
#define ADMIN_SSH_CONSOLE_MAX_SESSIONS 2U
|
||||
#define ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY 4096U
|
||||
#define ADMIN_SSH_CONSOLE_RESPONSE_RESERVE 512U
|
||||
#define ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH 4U
|
||||
#define ADMIN_SSH_CONSOLE_TASK_STACK_SIZE 12288U
|
||||
#define ADMIN_SSH_CONSOLE_TASK_PRIORITY 4U
|
||||
#define ADMIN_UART_CONSOLE_TASK_STACK_SIZE 6144U
|
||||
#define ADMIN_UART_CONSOLE_TASK_PRIORITY 3U
|
||||
#define ADMIN_SSH_CONSOLE_MAX_ARGUMENTS 10U
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool command_pending;
|
||||
bool executing;
|
||||
admin_ssh_console_token_t token;
|
||||
user_principal_t principal;
|
||||
size_t input_length;
|
||||
uint8_t input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
size_t output_start;
|
||||
size_t output_length;
|
||||
uint8_t output[ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY];
|
||||
} admin_session_t;
|
||||
|
||||
typedef enum {
|
||||
ADMIN_REQUEST_SSH = 0,
|
||||
ADMIN_REQUEST_UART0,
|
||||
} admin_request_origin_t;
|
||||
|
||||
typedef struct {
|
||||
admin_request_origin_t origin;
|
||||
admin_ssh_console_token_t token;
|
||||
user_principal_t principal;
|
||||
TaskHandle_t completion_task;
|
||||
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
} admin_request_t;
|
||||
|
||||
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static admin_session_t s_sessions[ADMIN_SSH_CONSOLE_MAX_SESSIONS];
|
||||
|
||||
static StaticQueue_t s_request_queue_storage;
|
||||
static uint8_t s_request_queue_bytes[ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH *
|
||||
sizeof(admin_request_t)];
|
||||
static QueueHandle_t s_request_queue;
|
||||
static TaskHandle_t s_task;
|
||||
static TaskHandle_t s_uart_task;
|
||||
static bool s_initialized;
|
||||
static bool s_dispatch_ready;
|
||||
/* Accessed only by the single dispatcher task while a callback is running. */
|
||||
static bool s_dispatch_remote;
|
||||
static bool s_dispatch_output_previous_cr;
|
||||
static user_principal_t s_dispatch_principal;
|
||||
|
||||
bool admin_ssh_console_dispatch_is_remote(void)
|
||||
{
|
||||
return xTaskGetCurrentTaskHandle() == s_task && s_dispatch_remote;
|
||||
}
|
||||
|
||||
const user_principal_t *admin_ssh_console_dispatch_principal(void)
|
||||
{
|
||||
return admin_ssh_console_dispatch_is_remote() ? &s_dispatch_principal : NULL;
|
||||
}
|
||||
|
||||
static bool token_valid(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
return token != NULL && token->slot_index < ADMIN_SSH_CONSOLE_MAX_SESSIONS &&
|
||||
token->session_id != 0U && token->slot_generation != 0U;
|
||||
}
|
||||
|
||||
static bool token_identity_matches(const admin_session_t *session,
|
||||
const admin_ssh_console_token_t *token)
|
||||
{
|
||||
return token_valid(token) && session->token.session_id == token->session_id &&
|
||||
session->token.slot_generation == token->slot_generation;
|
||||
}
|
||||
|
||||
static bool token_matches(const admin_session_t *session,
|
||||
const admin_ssh_console_token_t *token)
|
||||
{
|
||||
return session->active && token_identity_matches(session, token);
|
||||
}
|
||||
|
||||
static bool append_output_locked(admin_session_t *session,
|
||||
const uint8_t *data, size_t length)
|
||||
{
|
||||
if (data == NULL || length > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
|
||||
return false;
|
||||
}
|
||||
size_t write_offset = (session->output_start + session->output_length) %
|
||||
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY;
|
||||
size_t first = length;
|
||||
if (first > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - write_offset) {
|
||||
first = ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - write_offset;
|
||||
}
|
||||
memcpy(session->output + write_offset, data, first);
|
||||
if (length > first) {
|
||||
memcpy(session->output, data + first, length - first);
|
||||
}
|
||||
session->output_length += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool worker_write(const admin_ssh_console_token_t *token,
|
||||
const char *text)
|
||||
{
|
||||
if (!token_valid(token) || text == NULL) {
|
||||
return false;
|
||||
}
|
||||
size_t length = strlen(text);
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
bool written = token_matches(session, token) &&
|
||||
append_output_locked(session, (const uint8_t *)text, length);
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return written;
|
||||
}
|
||||
|
||||
|
||||
static void print_prompt(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
(void)worker_write(token, "admin@serial-tool> ");
|
||||
}
|
||||
|
||||
|
||||
static int ssh_output_write(void *cookie, const char *buffer, int length)
|
||||
{
|
||||
const admin_ssh_console_token_t *token = cookie;
|
||||
if (!token_valid(token) || buffer == NULL || length <= 0) {
|
||||
return length == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
size_t offset = 0U;
|
||||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(5000U);
|
||||
while (offset < (size_t)length) {
|
||||
const uint8_t value = (uint8_t)buffer[offset];
|
||||
uint8_t translated[2] = {value, 0U};
|
||||
size_t translated_length = 1U;
|
||||
if (value == '\n' && !s_dispatch_output_previous_cr) {
|
||||
translated[0] = '\r';
|
||||
translated[1] = '\n';
|
||||
translated_length = 2U;
|
||||
}
|
||||
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
bool active = token_matches(session, token);
|
||||
size_t available = active
|
||||
? ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length
|
||||
: 0U;
|
||||
bool written = active && available >= translated_length &&
|
||||
append_output_locked(session, translated, translated_length);
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
|
||||
if (!active) {
|
||||
errno = EPIPE;
|
||||
return offset == 0U ? -1 : (int)offset;
|
||||
}
|
||||
if (written) {
|
||||
s_dispatch_output_previous_cr = value == '\r';
|
||||
++offset;
|
||||
continue;
|
||||
}
|
||||
if ((int32_t)(xTaskGetTickCount() - deadline) >= 0) {
|
||||
errno = EAGAIN;
|
||||
return offset == 0U ? -1 : (int)offset;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(5U));
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
static bool remote_command_allowed(const admin_request_t *request)
|
||||
{
|
||||
char copy[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
memcpy(copy, request->line, sizeof(copy));
|
||||
char *argv[ADMIN_SSH_CONSOLE_MAX_ARGUMENTS] = {0};
|
||||
/* Use exactly the same quote/escape parser as esp_console_run(). */
|
||||
size_t argc = esp_console_split_argv(copy, argv, ADMIN_SSH_CONSOLE_MAX_ARGUMENTS);
|
||||
bool allowed = argc > 0U;
|
||||
if (allowed && strcmp(argv[0], "reboot") == 0) {
|
||||
allowed = false;
|
||||
} else if (allowed && strcmp(argv[0], "ping") == 0) {
|
||||
allowed = false;
|
||||
} else if (allowed && strcmp(argv[0], "user") == 0 && argc >= 2U) {
|
||||
if (strcmp(argv[1], "bootstrap") == 0 || strcmp(argv[1], "recover") == 0) {
|
||||
allowed = false;
|
||||
} else if (strcmp(argv[1], "add") == 0) {
|
||||
allowed = argc == 5U && strcmp(argv[4], "--generate") == 0;
|
||||
} else if (strcmp(argv[1], "password") == 0) {
|
||||
allowed = argc == 4U && strcmp(argv[3], "--generate") == 0;
|
||||
} else if (strcmp(argv[1], "key") == 0 && argc >= 3U &&
|
||||
strcmp(argv[2], "add") == 0) {
|
||||
allowed = argc == 6U;
|
||||
}
|
||||
} else if (allowed && strcmp(argv[0], "wifi") == 0 && argc >= 2U) {
|
||||
if (strcmp(argv[1], "ping") == 0) {
|
||||
allowed = false;
|
||||
} else if (strcmp(argv[1], "profile") == 0 && argc >= 3U &&
|
||||
strcmp(argv[2], "secret") == 0) {
|
||||
allowed = false;
|
||||
} else if (strcmp(argv[1], "ap") == 0 && argc >= 3U &&
|
||||
(strcmp(argv[2], "secret") == 0 ||
|
||||
strcmp(argv[2], "show-secret") == 0)) {
|
||||
allowed = false;
|
||||
}
|
||||
} else if (allowed && strcmp(argv[0], "web") == 0 && argc >= 2U) {
|
||||
allowed = strcmp(argv[1], "credentials") != 0 &&
|
||||
strcmp(argv[1], "certificate") != 0 &&
|
||||
strcmp(argv[1], "reset") != 0;
|
||||
} else if (allowed && strcmp(argv[0], "ssh") == 0 && argc >= 2U) {
|
||||
allowed = strcmp(argv[1], "start") != 0 && strcmp(argv[1], "stop") != 0 &&
|
||||
strcmp(argv[1], "disconnect") != 0 && strcmp(argv[1], "reset") != 0;
|
||||
if (allowed && strcmp(argv[1], "host-key") == 0) {
|
||||
allowed = argc == 3U && strcmp(argv[2], "info") == 0;
|
||||
}
|
||||
}
|
||||
secure_wipe(copy, sizeof(copy));
|
||||
return allowed;
|
||||
}
|
||||
|
||||
static void report_command_result(esp_err_t error, int command_result)
|
||||
{
|
||||
if (error == ESP_ERR_NOT_FOUND) {
|
||||
printf("Unrecognized command\n");
|
||||
} else if (error == ESP_OK && command_result != 0) {
|
||||
printf("Command returned non-zero error code: 0x%x (%s)\n",
|
||||
command_result, esp_err_to_name(command_result));
|
||||
} else if (error != ESP_OK && error != ESP_ERR_INVALID_ARG) {
|
||||
printf("Internal error: %s\n", esp_err_to_name(error));
|
||||
}
|
||||
}
|
||||
|
||||
static void dispatch_registered_command(admin_request_t *request)
|
||||
{
|
||||
FILE *saved_stdout = stdout;
|
||||
FILE *saved_stderr = stderr;
|
||||
FILE *remote_stream = NULL;
|
||||
if (request->origin == ADMIN_REQUEST_SSH) {
|
||||
remote_stream = funopen(&request->token, NULL, ssh_output_write, NULL, NULL);
|
||||
if (remote_stream == NULL) {
|
||||
(void)worker_write(&request->token, "Could not create command output stream.\r\n");
|
||||
return;
|
||||
}
|
||||
setvbuf(remote_stream, NULL, _IONBF, 0);
|
||||
stdout = remote_stream;
|
||||
stderr = remote_stream;
|
||||
s_dispatch_output_previous_cr = false;
|
||||
s_dispatch_remote = true;
|
||||
s_dispatch_principal = request->principal;
|
||||
} else {
|
||||
s_dispatch_remote = false;
|
||||
secure_wipe(&s_dispatch_principal, sizeof(s_dispatch_principal));
|
||||
}
|
||||
|
||||
int command_result = 0;
|
||||
esp_err_t error = esp_console_run((const char *)request->line, &command_result);
|
||||
report_command_result(error, command_result);
|
||||
fflush(stdout);
|
||||
|
||||
s_dispatch_remote = false;
|
||||
s_dispatch_output_previous_cr = false;
|
||||
secure_wipe(&s_dispatch_principal, sizeof(s_dispatch_principal));
|
||||
if (remote_stream != NULL) {
|
||||
stdout = saved_stdout;
|
||||
stderr = saved_stderr;
|
||||
fclose(remote_stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void worker_task(void *context)
|
||||
{
|
||||
(void)context;
|
||||
for (;;) {
|
||||
admin_request_t request;
|
||||
if (xQueueReceive(s_request_queue, &request, portMAX_DELAY) != pdTRUE) {
|
||||
continue;
|
||||
}
|
||||
if (request.origin == ADMIN_REQUEST_UART0) {
|
||||
dispatch_registered_command(&request);
|
||||
if (request.completion_task != NULL) {
|
||||
xTaskNotifyGive(request.completion_task);
|
||||
}
|
||||
secure_wipe(&request, sizeof(request));
|
||||
continue;
|
||||
}
|
||||
|
||||
bool current = false;
|
||||
esp_err_t auth_error = user_database_principal_is_current(&request.principal, ¤t);
|
||||
bool active;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[request.token.slot_index];
|
||||
active = token_matches(session, &request.token) && session->command_pending &&
|
||||
!session->executing;
|
||||
if (active) {
|
||||
session->executing = true;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
bool authorized = active && auth_error == ESP_OK && current &&
|
||||
request.principal.role == USER_ROLE_ADMIN &&
|
||||
remote_command_allowed(&request);
|
||||
if (authorized) {
|
||||
dispatch_registered_command(&request);
|
||||
} else if (active) {
|
||||
(void)worker_write(&request.token,
|
||||
auth_error == ESP_OK && current
|
||||
? "Command is restricted to physical UART0.\r\n"
|
||||
: "Administrative authorization is no longer current; closing session.\r\n");
|
||||
}
|
||||
bool prompt = false;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
session = &s_sessions[request.token.slot_index];
|
||||
if (token_matches(session, &request.token)) {
|
||||
session->executing = false;
|
||||
session->command_pending = false;
|
||||
prompt = auth_error == ESP_OK && current &&
|
||||
request.principal.role == USER_ROLE_ADMIN;
|
||||
} else if (!session->active && session->executing &&
|
||||
token_identity_matches(session, &request.token)) {
|
||||
/* A disconnect invalidated this executing request; erase buffered secrets. */
|
||||
secure_wipe(session, sizeof(*session));
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (prompt) {
|
||||
print_prompt(&request.token);
|
||||
}
|
||||
secure_wipe(&request, sizeof(request));
|
||||
}
|
||||
}
|
||||
|
||||
static void uart_frontend_task(void *context)
|
||||
{
|
||||
(void)context;
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
linenoiseSetMaxLineLen(ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY);
|
||||
printf("\r\nType 'help' to get the list of commands.\r\n"
|
||||
"Use UP/DOWN arrows for history and TAB for completion.\r\n");
|
||||
|
||||
for (;;) {
|
||||
char *line = linenoise("serial-tool> ");
|
||||
if (line == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (line[0] != '\0') {
|
||||
(void)linenoiseHistoryAdd(line);
|
||||
}
|
||||
|
||||
admin_request_t request = {
|
||||
.origin = ADMIN_REQUEST_UART0,
|
||||
.completion_task = xTaskGetCurrentTaskHandle(),
|
||||
};
|
||||
strlcpy((char *)request.line, line, sizeof(request.line));
|
||||
linenoiseFree(line);
|
||||
|
||||
(void)ulTaskNotifyTake(pdTRUE, 0U);
|
||||
if (xQueueSend(s_request_queue, &request, portMAX_DELAY) == pdTRUE) {
|
||||
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
} else {
|
||||
printf("Administrative command queue unavailable.\n");
|
||||
}
|
||||
secure_wipe(&request, sizeof(request));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t admin_ssh_console_init(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
if (s_initialized) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
s_request_queue = xQueueCreateStatic(ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH,
|
||||
sizeof(admin_request_t), s_request_queue_bytes,
|
||||
&s_request_queue_storage);
|
||||
if (s_request_queue == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (xTaskCreate(worker_task, "admin_ssh_console", ADMIN_SSH_CONSOLE_TASK_STACK_SIZE,
|
||||
NULL, ADMIN_SSH_CONSOLE_TASK_PRIORITY, &s_task) != pdPASS) {
|
||||
s_task = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
s_initialized = true;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t admin_ssh_console_start_uart_frontend(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool initialized = s_initialized;
|
||||
bool already_started = s_dispatch_ready;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (already_started) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (xTaskCreate(uart_frontend_task, "admin_uart_console",
|
||||
ADMIN_UART_CONSOLE_TASK_STACK_SIZE, NULL,
|
||||
ADMIN_UART_CONSOLE_TASK_PRIORITY, &s_uart_task) != pdPASS) {
|
||||
s_uart_task = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
s_dispatch_ready = true;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
|
||||
const user_principal_t *principal)
|
||||
{
|
||||
if (!token_valid(token) || principal == NULL || principal->role != USER_ROLE_ADMIN) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool ready = s_initialized && s_dispatch_ready;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!ready) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
bool current = false;
|
||||
if (user_database_principal_is_current(principal, ¤t) != ESP_OK || !current) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
if (session->executing) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
secure_wipe(session, sizeof(*session));
|
||||
session->active = true;
|
||||
session->token = *token;
|
||||
session->principal = *principal;
|
||||
static const char banner[] =
|
||||
"ESP32 Serial Swiss Army Knife administrative SSH shell\r\n";
|
||||
static const char prompt[] =
|
||||
"Run 'help' for supported remote administrative commands.\r\nadmin@serial-tool> ";
|
||||
(void)append_output_locked(session, (const uint8_t *)banner, sizeof(banner) - 1U);
|
||||
(void)append_output_locked(session, (const uint8_t *)prompt, sizeof(prompt) - 1U);
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void admin_ssh_console_close(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
if (!token_valid(token)) {
|
||||
return;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
bool matched = token_matches(session, token);
|
||||
if (matched) {
|
||||
session->active = false;
|
||||
if (!session->executing) {
|
||||
secure_wipe(session, sizeof(*session));
|
||||
}
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
if (!token_valid(token)) {
|
||||
return false;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
bool accepts = token_matches(session, token) && !session->command_pending &&
|
||||
session->output_length <= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
|
||||
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return accepts;
|
||||
}
|
||||
|
||||
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
const uint8_t *data, size_t length,
|
||||
size_t *consumed)
|
||||
{
|
||||
if (consumed == NULL || !token_valid(token) || (data == NULL && length != 0U)) {
|
||||
return false;
|
||||
}
|
||||
*consumed = 0U;
|
||||
for (size_t index = 0U; index < length; ++index) {
|
||||
admin_request_t request = {0};
|
||||
bool submit = false;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
if (!token_matches(session, token) || session->command_pending) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return *consumed != 0U;
|
||||
}
|
||||
uint8_t value = data[index];
|
||||
if (session->output_length >= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
|
||||
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return *consumed != 0U;
|
||||
}
|
||||
if (value == '\r' || value == '\n') {
|
||||
if (value == '\n' && session->input_length == 0U) {
|
||||
++*consumed;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
continue;
|
||||
}
|
||||
memcpy(request.line, session->input, session->input_length);
|
||||
request.origin = ADMIN_REQUEST_SSH;
|
||||
request.token = *token;
|
||||
request.principal = session->principal;
|
||||
session->input_length = 0U;
|
||||
session->command_pending = true;
|
||||
(void)append_output_locked(session, (const uint8_t *)"\r\n",
|
||||
sizeof("\r\n") - 1U);
|
||||
submit = true;
|
||||
} else if (value == 0x03U) {
|
||||
session->input_length = 0U;
|
||||
(void)append_output_locked(session, (const uint8_t *)"^C\r\n",
|
||||
sizeof("^C\r\n") - 1U);
|
||||
(void)append_output_locked(session, (const uint8_t *)"admin@serial-tool> ",
|
||||
sizeof("admin@serial-tool> ") - 1U);
|
||||
} else if (value == 0x08U || value == 0x7fU) {
|
||||
if (session->input_length > 0U) {
|
||||
--session->input_length;
|
||||
(void)append_output_locked(session, (const uint8_t *)"\b \b",
|
||||
sizeof("\b \b") - 1U);
|
||||
}
|
||||
} else if (value >= 0x20U && value <= 0x7eU) {
|
||||
if (session->input_length >= ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY) {
|
||||
session->input_length = 0U;
|
||||
(void)append_output_locked(session, (const uint8_t *)
|
||||
"\r\nCommand too long; discarded.\r\nadmin@serial-tool> ",
|
||||
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
|
||||
} else {
|
||||
session->input[session->input_length++] = value;
|
||||
(void)append_output_locked(session, &value, 1U);
|
||||
}
|
||||
}
|
||||
++*consumed;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (submit && xQueueSend(s_request_queue, &request, 0U) != pdTRUE) {
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
session = &s_sessions[token->slot_index];
|
||||
if (token_matches(session, token)) {
|
||||
session->command_pending = false;
|
||||
(void)append_output_locked(session, (const uint8_t *)
|
||||
"Administrative command queue is busy.\r\nadmin@serial-tool> ",
|
||||
sizeof("Administrative command queue is busy.\r\nadmin@serial-tool> ") - 1U);
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
secure_wipe(&request, sizeof(request));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
|
||||
uint8_t *data, size_t capacity,
|
||||
size_t *received)
|
||||
{
|
||||
if (received == NULL || data == NULL || capacity == 0U || !token_valid(token)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*received = 0U;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
if (!token_matches(session, token)) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
size_t copied = session->output_length < capacity ? session->output_length : capacity;
|
||||
size_t first = copied;
|
||||
if (first > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_start) {
|
||||
first = ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_start;
|
||||
}
|
||||
memcpy(data, session->output + session->output_start, first);
|
||||
if (copied > first) {
|
||||
memcpy(data + first, session->output, copied - first);
|
||||
}
|
||||
session->output_start = (session->output_start + copied) %
|
||||
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY;
|
||||
session->output_length -= copied;
|
||||
*received = copied;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t admin_ssh_console_get_session_snapshot(
|
||||
const admin_ssh_console_token_t *token,
|
||||
admin_ssh_console_session_snapshot_t *snapshot)
|
||||
{
|
||||
if (snapshot == NULL || !token_valid(token)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memset(snapshot, 0, sizeof(*snapshot));
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
admin_session_t *session = &s_sessions[token->slot_index];
|
||||
if (!token_matches(session, token)) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
snapshot->active = true;
|
||||
snapshot->command_pending = session->command_pending;
|
||||
snapshot->input_pending = session->input_length != 0U;
|
||||
snapshot->output_pending = session->output_length != 0U;
|
||||
snapshot->input_length = session->input_length;
|
||||
snapshot->output_length = session->output_length;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Bounded, transport-neutral administrative command worker for SSH sessions. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "user_database.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Fits the longest supported ECDSA P-256 OpenSSH key import command. */
|
||||
#define ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY 256U
|
||||
|
||||
typedef struct {
|
||||
uint8_t slot_index;
|
||||
uint32_t session_id;
|
||||
uint32_t slot_generation;
|
||||
} admin_ssh_console_token_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool command_pending;
|
||||
bool input_pending;
|
||||
bool output_pending;
|
||||
size_t input_length;
|
||||
size_t output_length;
|
||||
} admin_ssh_console_session_snapshot_t;
|
||||
|
||||
/* Starts the single command worker. It is the sole esp_console_run() caller. */
|
||||
esp_err_t admin_ssh_console_init(void);
|
||||
/* Called after all ESP-IDF commands are registered; starts the UART0 frontend. */
|
||||
esp_err_t admin_ssh_console_start_uart_frontend(void);
|
||||
|
||||
/* Valid only while a registered command callback runs on the dispatcher task. */
|
||||
bool admin_ssh_console_dispatch_is_remote(void);
|
||||
const user_principal_t *admin_ssh_console_dispatch_principal(void);
|
||||
|
||||
/* The token and principal are copied; no SSH or socket objects cross this boundary. */
|
||||
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
|
||||
const user_principal_t *principal);
|
||||
void admin_ssh_console_close(const admin_ssh_console_token_t *token);
|
||||
|
||||
/* Called only by the SSH owner task. Returns false when input must be backpressured. */
|
||||
bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token);
|
||||
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
const uint8_t *data, size_t length,
|
||||
size_t *consumed);
|
||||
|
||||
/* Called only by the SSH owner task; copies already-produced output without blocking. */
|
||||
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
|
||||
uint8_t *data, size_t capacity,
|
||||
size_t *received);
|
||||
esp_err_t admin_ssh_console_get_session_snapshot(
|
||||
const admin_ssh_console_token_t *token,
|
||||
admin_ssh_console_session_snapshot_t *snapshot);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "admin_ssh_console.h"
|
||||
#include "driver/uart.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -31,6 +32,10 @@ static esp_err_t read_input(const char *prompt, uint8_t *output, size_t capacity
|
||||
if (prompt == NULL || output == NULL || output_length == NULL || capacity == 0U) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (admin_ssh_console_dispatch_is_remote()) {
|
||||
printf("Interactive input is restricted to physical UART0.\n");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
*output_length = 0U;
|
||||
memset(output, 0, capacity);
|
||||
esp_err_t error = prepare_prompt(prompt);
|
||||
|
||||
+9
-4
@@ -1,6 +1,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/uart.h"
|
||||
#include "admin_ssh_console.h"
|
||||
#include "console_completion.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_err.h"
|
||||
@@ -64,6 +65,8 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(status_led_init());
|
||||
ESP_ERROR_CHECK(rs232_port_owner_init());
|
||||
ESP_ERROR_CHECK(rs232_hw_test_init());
|
||||
/* Reserve the shared UART0 dispatcher before optional SSH/network services. */
|
||||
ESP_ERROR_CHECK(admin_ssh_console_init());
|
||||
|
||||
/* The optional display can fail without affecting UART0 or serial transports. */
|
||||
esp_err_t local_display_error = local_display_init();
|
||||
@@ -274,8 +277,9 @@ void app_main(void)
|
||||
|
||||
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||
repl_config.prompt = "serial-tool> ";
|
||||
repl_config.max_cmdline_length = 160;
|
||||
repl_config.task_stack_size = 8192;
|
||||
repl_config.max_cmdline_length = ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY;
|
||||
/* The stock REPL task remains dormant; our shared frontend owns line dispatch. */
|
||||
repl_config.task_stack_size = 2048;
|
||||
|
||||
/*
|
||||
* UART0 remains dedicated to development and diagnostics. The external
|
||||
@@ -304,8 +308,9 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(system_console_register_commands());
|
||||
/* Upgrade late UART terminals safely and add nested completion. */
|
||||
console_completion_install();
|
||||
ESP_ERROR_CHECK(esp_console_start_repl(repl));
|
||||
ESP_ERROR_CHECK(admin_ssh_console_start_uart_frontend());
|
||||
|
||||
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE);
|
||||
ESP_LOGI(TAG, "Shared UART0/SSH administration console ready at %d baud",
|
||||
CONSOLE_BAUD_RATE);
|
||||
ESP_LOGI(TAG, "Type 'help' for commands; native USB starts UART1 only when its host port opens");
|
||||
}
|
||||
|
||||
+26
-12
@@ -50,6 +50,15 @@ static const char *auth_method_name(user_auth_method_t method)
|
||||
: method == USER_AUTH_METHOD_SSH_PUBLIC_KEY ? "public-key" : "unknown";
|
||||
}
|
||||
|
||||
static const char *route_name(ssh_transport_session_route_t route)
|
||||
{
|
||||
return route == SSH_TRANSPORT_ROUTE_BROKER
|
||||
? "broker"
|
||||
: route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE
|
||||
? "admin-console"
|
||||
: "none";
|
||||
}
|
||||
|
||||
static int print_sessions(const ssh_transport_snapshot_t *snapshot)
|
||||
{
|
||||
printf("SSH sessions: active=%" PRIu32 "/%u\n",
|
||||
@@ -60,21 +69,22 @@ static int print_sessions(const ssh_transport_snapshot_t *snapshot)
|
||||
continue;
|
||||
}
|
||||
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",
|
||||
" user-role=%s method=%s route=%s broker=%" PRIu32
|
||||
" broker-role=%s admin-command=%s admin-output=%" PRIu32
|
||||
" 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"
|
||||
: (session->writer ? "writer" : "observer"),
|
||||
session->principal_valid ? user_role_to_string(session->user_role) : "-",
|
||||
session->principal_valid ? auth_method_name(session->auth_method) : "-",
|
||||
route_name(session->route), session->broker_client_id,
|
||||
session->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE
|
||||
? "n/a"
|
||||
: (session->broker_client_id == SESSION_BROKER_NO_CLIENT
|
||||
? "unattached"
|
||||
: (session->writer ? "writer" : "observer")),
|
||||
session->admin_command_pending ? "running" : "idle",
|
||||
session->admin_output_pending,
|
||||
session->rx_pending ? "yes" : "no",
|
||||
session->tx_pending ? "yes" : "no",
|
||||
session->close_requested ? "yes" : "no");
|
||||
@@ -135,6 +145,10 @@ static int show_counters(void)
|
||||
counter->disconnections, counter->writer_requests,
|
||||
counter->writer_grants, counter->writer_denials,
|
||||
counter->writer_revocations);
|
||||
printf("Admin console: admissions=%" PRIu64 " admission-failures=%" PRIu64
|
||||
" input-backpressure=%" PRIu64 "\n",
|
||||
counter->admin_console_admissions, counter->admin_console_admission_failures,
|
||||
counter->admin_console_input_rejections);
|
||||
printf("Stream: rx=%" PRIu64 " accepted=%" PRIu64
|
||||
" rejected=%" PRIu64 " tx=%" PRIu64
|
||||
" io-failures=%" PRIu64 " session-revocations=%" PRIu64 "\n",
|
||||
|
||||
+148
-11
@@ -9,6 +9,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "admin_ssh_console.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
@@ -57,6 +58,7 @@ typedef struct {
|
||||
int socket_fd;
|
||||
WOLFSSH *ssh;
|
||||
session_broker_client_id_t broker_client_id;
|
||||
ssh_transport_session_route_t route;
|
||||
user_principal_t principal;
|
||||
user_principal_t pending_principal;
|
||||
bool principal_valid;
|
||||
@@ -125,6 +127,16 @@ static void notify_task(void)
|
||||
}
|
||||
}
|
||||
|
||||
static admin_ssh_console_token_t admin_console_token(const ssh_slot_t *slot,
|
||||
size_t slot_index)
|
||||
{
|
||||
return (admin_ssh_console_token_t){
|
||||
.slot_index = (uint8_t)slot_index,
|
||||
.session_id = slot->session_id,
|
||||
.slot_generation = slot->generation,
|
||||
};
|
||||
}
|
||||
|
||||
static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
|
||||
{
|
||||
ssh_transport_session_snapshot_t snapshot = {
|
||||
@@ -140,11 +152,21 @@ 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,
|
||||
.route = slot->route,
|
||||
.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->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
|
||||
admin_ssh_console_session_snapshot_t admin_snapshot;
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
if (admin_ssh_console_get_session_snapshot(&token, &admin_snapshot) == ESP_OK) {
|
||||
snapshot.admin_command_pending = admin_snapshot.command_pending;
|
||||
snapshot.admin_output_pending = (uint32_t)admin_snapshot.output_length;
|
||||
snapshot.tx_pending = snapshot.tx_pending || admin_snapshot.output_pending;
|
||||
}
|
||||
}
|
||||
if (slot->principal_valid) {
|
||||
memcpy(snapshot.username, slot->principal.username,
|
||||
slot->principal.username_length);
|
||||
@@ -442,6 +464,11 @@ static void close_socket(int *socket_fd)
|
||||
|
||||
static bool cleanup_slot(ssh_slot_t *slot)
|
||||
{
|
||||
if (slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
|
||||
size_t slot_index = (size_t)(slot - s_slots);
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
admin_ssh_console_close(&token);
|
||||
}
|
||||
if (slot->ssh != NULL) {
|
||||
(void)wolfSSH_shutdown(slot->ssh);
|
||||
wolfSSH_free(slot->ssh);
|
||||
@@ -886,14 +913,33 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t error = connect_broker(slot, slot_index);
|
||||
if (error != ESP_OK) {
|
||||
add_counter(&s_counters.broker_failures, 1U);
|
||||
request_slot_close(slot, false);
|
||||
esp_err_t error;
|
||||
if (slot->principal.role == USER_ROLE_USER) {
|
||||
error = connect_broker(slot, slot_index);
|
||||
if (error != ESP_OK) {
|
||||
add_counter(&s_counters.broker_failures, 1U);
|
||||
request_slot_close(slot, false);
|
||||
return;
|
||||
}
|
||||
slot->route = SSH_TRANSPORT_ROUTE_BROKER;
|
||||
} else if (slot->principal.role == USER_ROLE_ADMIN) {
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
error = admin_ssh_console_open(&token, &slot->principal);
|
||||
if (error != ESP_OK) {
|
||||
add_counter(&s_counters.admin_console_admission_failures, 1U);
|
||||
request_slot_close(slot, false);
|
||||
return;
|
||||
}
|
||||
slot->route = SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
|
||||
add_counter(&s_counters.admin_console_admissions, 1U);
|
||||
} else {
|
||||
request_slot_close(slot, true);
|
||||
return;
|
||||
}
|
||||
if (!slot_principal_is_current(slot)) {
|
||||
disconnect_failed_admission(slot);
|
||||
if (slot->route == SSH_TRANSPORT_ROUTE_BROKER) {
|
||||
disconnect_failed_admission(slot);
|
||||
}
|
||||
request_slot_close(slot, true);
|
||||
return;
|
||||
}
|
||||
@@ -1086,12 +1132,98 @@ static bool read_broker_output(ssh_slot_t *slot)
|
||||
return received == 0U ? true : flush_client_output(slot);
|
||||
}
|
||||
|
||||
static void process_active(ssh_slot_t *slot)
|
||||
static bool reconcile_admin_principal(ssh_slot_t *slot)
|
||||
{
|
||||
bool healthy = service_wolfssh_io(slot) &&
|
||||
drain_broker_events(slot) && reconcile_writer(slot) &&
|
||||
flush_client_output(slot) && read_broker_output(slot) &&
|
||||
flush_client_input(slot) && receive_client_input(slot);
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (now - slot->last_reconcile_us < SSH_TRANSPORT_RECONCILE_INTERVAL_US) {
|
||||
return true;
|
||||
}
|
||||
slot->last_reconcile_us = now;
|
||||
if (!slot_principal_is_current(slot) || slot->principal.role != USER_ROLE_ADMIN) {
|
||||
request_slot_close(slot, true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool flush_admin_input(ssh_slot_t *slot, size_t slot_index)
|
||||
{
|
||||
if (slot->rx_offset >= slot->rx_length) {
|
||||
slot->rx_offset = 0U;
|
||||
slot->rx_length = 0U;
|
||||
return true;
|
||||
}
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
size_t consumed = 0U;
|
||||
bool accepted = admin_ssh_console_feed_input(
|
||||
&token, slot->rx_buffer + slot->rx_offset,
|
||||
slot->rx_length - slot->rx_offset, &consumed);
|
||||
if (consumed > 0U) {
|
||||
slot->rx_offset += consumed;
|
||||
add_counter(&s_counters.rx_accepted_bytes, consumed);
|
||||
}
|
||||
if (slot->rx_offset >= slot->rx_length) {
|
||||
slot->rx_offset = 0U;
|
||||
slot->rx_length = 0U;
|
||||
}
|
||||
if (!accepted && consumed == 0U) {
|
||||
add_counter(&s_counters.admin_console_input_rejections, 1U);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool receive_admin_input(ssh_slot_t *slot, size_t slot_index)
|
||||
{
|
||||
if (slot->rx_length != 0U) {
|
||||
return flush_admin_input(slot, slot_index);
|
||||
}
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
if (!admin_ssh_console_accepts_input(&token)) {
|
||||
return true;
|
||||
}
|
||||
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
|
||||
int result = wolfSSH_stream_read(slot->ssh, slot->rx_buffer,
|
||||
sizeof(slot->rx_buffer));
|
||||
if (result > 0) {
|
||||
slot->rx_offset = 0U;
|
||||
slot->rx_length = (size_t)result;
|
||||
add_counter(&s_counters.rx_bytes, (uint64_t)result);
|
||||
return flush_admin_input(slot, slot_index);
|
||||
}
|
||||
return result == 0 || wolfssh_would_block(slot->ssh, result);
|
||||
}
|
||||
|
||||
static bool read_admin_output(ssh_slot_t *slot, size_t slot_index)
|
||||
{
|
||||
if (slot->tx_length != 0U) {
|
||||
return true;
|
||||
}
|
||||
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
|
||||
size_t received = 0U;
|
||||
esp_err_t error = admin_ssh_console_read_output(&token, slot->tx_buffer,
|
||||
sizeof(slot->tx_buffer), &received);
|
||||
if (error != ESP_OK && error != ESP_ERR_NOT_FOUND) {
|
||||
return false;
|
||||
}
|
||||
slot->tx_offset = 0U;
|
||||
slot->tx_length = received;
|
||||
return error == ESP_OK;
|
||||
}
|
||||
|
||||
static void process_active(ssh_slot_t *slot, size_t slot_index)
|
||||
{
|
||||
bool healthy = service_wolfssh_io(slot);
|
||||
if (healthy && slot->route == SSH_TRANSPORT_ROUTE_BROKER) {
|
||||
healthy = drain_broker_events(slot) && reconcile_writer(slot) &&
|
||||
flush_client_output(slot) && read_broker_output(slot) &&
|
||||
flush_client_input(slot) && receive_client_input(slot);
|
||||
} else if (healthy && slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
|
||||
healthy = reconcile_admin_principal(slot) && flush_client_output(slot) &&
|
||||
read_admin_output(slot, slot_index) && flush_client_output(slot) &&
|
||||
receive_admin_input(slot, slot_index);
|
||||
} else if (healthy) {
|
||||
healthy = false;
|
||||
}
|
||||
if (!healthy) {
|
||||
add_counter(&s_counters.io_failures, 1U);
|
||||
request_slot_close(slot, false);
|
||||
@@ -1117,7 +1249,7 @@ static void process_slots(void)
|
||||
if (slot->state == SSH_TRANSPORT_SESSION_HANDSHAKE) {
|
||||
process_handshake(slot, index);
|
||||
} else if (slot->state == SSH_TRANSPORT_SESSION_ACTIVE) {
|
||||
process_active(slot);
|
||||
process_active(slot, index);
|
||||
}
|
||||
publish_slot(slot, index);
|
||||
}
|
||||
@@ -1186,6 +1318,11 @@ esp_err_t ssh_transport_init(void)
|
||||
error = ESP_ERR_NO_MEM;
|
||||
goto fail;
|
||||
}
|
||||
error = admin_ssh_console_init();
|
||||
if (error != ESP_OK) {
|
||||
vSemaphoreDelete(command_mutex);
|
||||
goto fail;
|
||||
}
|
||||
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
|
||||
s_slots[index].state = SSH_TRANSPORT_SESSION_FREE;
|
||||
s_slots[index].socket_fd = -1;
|
||||
|
||||
@@ -27,6 +27,12 @@ typedef enum {
|
||||
SSH_TRANSPORT_SESSION_CLOSING,
|
||||
} ssh_transport_session_state_t;
|
||||
|
||||
typedef enum {
|
||||
SSH_TRANSPORT_ROUTE_NONE = 0,
|
||||
SSH_TRANSPORT_ROUTE_BROKER,
|
||||
SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE,
|
||||
} ssh_transport_session_route_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t starts;
|
||||
uint64_t start_failures;
|
||||
@@ -52,6 +58,9 @@ typedef struct {
|
||||
uint64_t tx_bytes;
|
||||
uint64_t io_failures;
|
||||
uint64_t session_revocations;
|
||||
uint64_t admin_console_admissions;
|
||||
uint64_t admin_console_admission_failures;
|
||||
uint64_t admin_console_input_rejections;
|
||||
} ssh_transport_counters_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -62,11 +71,14 @@ typedef struct {
|
||||
bool close_requested;
|
||||
bool rx_pending;
|
||||
bool tx_pending;
|
||||
bool admin_command_pending;
|
||||
uint32_t admin_output_pending;
|
||||
uint32_t session_id;
|
||||
uint32_t generation;
|
||||
int socket_fd;
|
||||
session_broker_client_id_t broker_client_id;
|
||||
ssh_transport_session_state_t state;
|
||||
ssh_transport_session_route_t route;
|
||||
user_role_t user_role;
|
||||
user_auth_method_t auth_method;
|
||||
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
|
||||
|
||||
+97
-40
@@ -7,6 +7,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "admin_command_gate.h"
|
||||
#include "admin_ssh_console.h"
|
||||
#include "console_input.h"
|
||||
#include "esp_console.h"
|
||||
#include "mbedtls/base64.h"
|
||||
@@ -18,6 +20,9 @@
|
||||
|
||||
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
|
||||
|
||||
/* `user` commands are serialized by the administration gate. */
|
||||
static user_database_snapshot_t s_user_snapshot;
|
||||
|
||||
static void print_usage(void)
|
||||
{
|
||||
printf("Usage:\n");
|
||||
@@ -30,6 +35,7 @@ static void print_usage(void)
|
||||
printf(" user role <username> <user|admin> --force\n");
|
||||
printf(" user password <username> [--generate]\n");
|
||||
printf(" user key add <username>\n");
|
||||
printf(" user key add <username> <type> <base64>\n");
|
||||
printf(" user key delete <username> <0..2> --force\n");
|
||||
printf(" user key clear <username> --force\n");
|
||||
}
|
||||
@@ -88,23 +94,22 @@ static void print_user(const user_database_user_snapshot_t *user)
|
||||
|
||||
static int show_users(const char *selected)
|
||||
{
|
||||
user_database_snapshot_t snapshot;
|
||||
esp_err_t error = user_database_get_snapshot(&snapshot);
|
||||
esp_err_t error = user_database_get_snapshot(&s_user_snapshot);
|
||||
if (error != ESP_OK) {
|
||||
printf("User database unavailable: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
if (selected == NULL) {
|
||||
printf("User database: generation=%lu users=%u/%u admins=%u bootstrapped=%s\n",
|
||||
(unsigned long)snapshot.generation,
|
||||
(unsigned int)snapshot.user_count,
|
||||
(unsigned long)s_user_snapshot.generation,
|
||||
(unsigned int)s_user_snapshot.user_count,
|
||||
USER_DATABASE_MAX_USERS,
|
||||
(unsigned int)snapshot.admin_count,
|
||||
snapshot.admin_bootstrapped ? "yes" : "no");
|
||||
(unsigned int)s_user_snapshot.admin_count,
|
||||
s_user_snapshot.admin_bootstrapped ? "yes" : "no");
|
||||
}
|
||||
bool found = false;
|
||||
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
|
||||
const user_database_user_snapshot_t *user = &snapshot.users[index];
|
||||
const user_database_user_snapshot_t *user = &s_user_snapshot.users[index];
|
||||
if (!user->active ||
|
||||
(selected != NULL &&
|
||||
(strlen(selected) != user->username_length ||
|
||||
@@ -118,7 +123,7 @@ static int show_users(const char *selected)
|
||||
printf("User '%s' not found.\n", selected);
|
||||
return 1;
|
||||
}
|
||||
if (!snapshot.admin_bootstrapped) {
|
||||
if (!s_user_snapshot.admin_bootstrapped) {
|
||||
printf("Administrative network access is not bootstrapped; use 'user bootstrap'.\n");
|
||||
}
|
||||
return 0;
|
||||
@@ -297,6 +302,42 @@ static bool key_delimiter(uint8_t value)
|
||||
return value == ' ' || value == '\t';
|
||||
}
|
||||
|
||||
static int add_key_parts(const char *username,
|
||||
const uint8_t *type, size_t type_length,
|
||||
const uint8_t *encoded, size_t encoded_length)
|
||||
{
|
||||
uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY] = {0};
|
||||
size_t blob_length = 0U;
|
||||
int decoded = mbedtls_base64_decode(blob, sizeof(blob), &blob_length,
|
||||
encoded, encoded_length);
|
||||
if (decoded != 0 || !user_database_key_valid(type, type_length, blob, blob_length)) {
|
||||
printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n");
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t key_index = 0U;
|
||||
esp_err_t error = user_database_add_ssh_key(
|
||||
(const uint8_t *)username, strlen(username), type, type_length,
|
||||
blob, blob_length, &key_index);
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
if (error != ESP_OK) {
|
||||
if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) {
|
||||
printf("Could not add SSH key: that public key is already assigned to this account.\n");
|
||||
} else if (error == ESP_ERR_NO_MEM) {
|
||||
printf("Could not add SSH key: the account already has %u keys.\n",
|
||||
USER_DATABASE_MAX_SSH_KEYS_PER_USER);
|
||||
} else {
|
||||
printf("Could not add SSH key: %s\n", esp_err_to_name(error));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static int add_key(const char *username)
|
||||
{
|
||||
uint8_t line[USER_CONSOLE_KEY_LINE_CAPACITY] = {0};
|
||||
@@ -338,42 +379,15 @@ static int add_key(const char *username)
|
||||
size_t encoded_length = encoded_end == NULL
|
||||
? remaining
|
||||
: (size_t)(encoded_end - encoded);
|
||||
uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY] = {0};
|
||||
size_t blob_length = 0U;
|
||||
int decoded = mbedtls_base64_decode(blob, sizeof(blob), &blob_length,
|
||||
encoded, encoded_length);
|
||||
if (decoded != 0 ||
|
||||
!user_database_key_valid(line, type_length, blob, blob_length)) {
|
||||
printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n");
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
secure_wipe(line, sizeof(line));
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t key_index = 0U;
|
||||
error = user_database_add_ssh_key((const uint8_t *)username, strlen(username),
|
||||
line, type_length, blob, blob_length, &key_index);
|
||||
secure_wipe(blob, sizeof(blob));
|
||||
int result = add_key_parts(username, line, type_length, encoded, encoded_length);
|
||||
secure_wipe(line, sizeof(line));
|
||||
if (error != ESP_OK) {
|
||||
if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) {
|
||||
printf("Could not add SSH key: that public key is already assigned to this account.\n");
|
||||
} else if (error == ESP_ERR_NO_MEM) {
|
||||
printf("Could not add SSH key: the account already has %u keys.\n",
|
||||
USER_DATABASE_MAX_SSH_KEYS_PER_USER);
|
||||
} else {
|
||||
printf("Could not add SSH key: %s\n", esp_err_to_name(error));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
return result;
|
||||
}
|
||||
|
||||
static int command_user(int argc, char **argv)
|
||||
static int command_user_inner(int argc, char **argv)
|
||||
{
|
||||
bool remote = admin_ssh_console_dispatch_is_remote();
|
||||
const user_principal_t *principal = admin_ssh_console_dispatch_principal();
|
||||
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0) ||
|
||||
(argc == 2 && strcmp(argv[1], "list") == 0)) {
|
||||
return show_users(NULL);
|
||||
@@ -383,6 +397,10 @@ static int command_user(int argc, char **argv)
|
||||
}
|
||||
if (argc == 3 && strcmp(argv[1], "recover") == 0 &&
|
||||
strcmp(argv[2], "--force") == 0) {
|
||||
if (remote) {
|
||||
printf("User database recovery is restricted to physical UART0.\n");
|
||||
return 1;
|
||||
}
|
||||
return recover_database();
|
||||
}
|
||||
if ((argc == 2 || argc == 3) && strcmp(argv[1], "bootstrap") == 0) {
|
||||
@@ -391,6 +409,10 @@ static int command_user(int argc, char **argv)
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
if (remote) {
|
||||
printf("Administrator bootstrap is restricted to physical UART0.\n");
|
||||
return 1;
|
||||
}
|
||||
return bootstrap(generated);
|
||||
}
|
||||
if ((argc == 4 || argc == 5) && strcmp(argv[1], "add") == 0) {
|
||||
@@ -399,6 +421,10 @@ static int command_user(int argc, char **argv)
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
if (remote && !generated) {
|
||||
printf("Interactive password entry is restricted to physical UART0; use --generate.\n");
|
||||
return 1;
|
||||
}
|
||||
return add_user(argv[2], argv[3], generated);
|
||||
}
|
||||
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
|
||||
@@ -438,12 +464,31 @@ static int command_user(int argc, char **argv)
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
if (remote && !generated) {
|
||||
printf("Interactive password entry is restricted to physical UART0; use --generate.\n");
|
||||
return 1;
|
||||
}
|
||||
if (remote && generated && principal != NULL &&
|
||||
strlen(argv[2]) == principal->username_length &&
|
||||
memcmp(argv[2], principal->username, principal->username_length) == 0) {
|
||||
printf("Remote generated-password changes for the current admin are disabled; use UART0.\n");
|
||||
return 1;
|
||||
}
|
||||
return change_password(argv[2], generated);
|
||||
}
|
||||
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "add") == 0) {
|
||||
if (remote) {
|
||||
printf("Interactive SSH key entry is restricted to physical UART0; provide type and Base64 arguments.\n");
|
||||
return 1;
|
||||
}
|
||||
return add_key(argv[3]);
|
||||
}
|
||||
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "add") == 0) {
|
||||
return add_key_parts(argv[3], (const uint8_t *)argv[4], strlen(argv[4]),
|
||||
(const uint8_t *)argv[5], strlen(argv[5]));
|
||||
}
|
||||
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
|
||||
strcmp(argv[2], "delete") == 0 && strcmp(argv[5], "--force") == 0) {
|
||||
uint8_t index;
|
||||
@@ -477,6 +522,18 @@ static int command_user(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int command_user(int argc, char **argv)
|
||||
{
|
||||
esp_err_t error = admin_command_gate_take();
|
||||
if (error != ESP_OK) {
|
||||
printf("Administrative command gate unavailable: %s\n", esp_err_to_name(error));
|
||||
return 1;
|
||||
}
|
||||
int result = command_user_inner(argc, argv);
|
||||
admin_command_gate_give();
|
||||
return result;
|
||||
}
|
||||
|
||||
esp_err_t user_console_register_commands(void)
|
||||
{
|
||||
const esp_console_cmd_t command = {
|
||||
|
||||
@@ -146,6 +146,7 @@ esp_err_t user_database_remove_ssh_key(const uint8_t *username,
|
||||
esp_err_t user_database_clear_ssh_keys(const uint8_t *username,
|
||||
size_t username_length);
|
||||
|
||||
|
||||
bool user_database_username_valid(const uint8_t *username, size_t length);
|
||||
bool user_database_password_valid(const uint8_t *password, size_t length);
|
||||
bool user_database_key_valid(const uint8_t *key_type, size_t key_type_length,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "admin_ssh_console.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_console.h"
|
||||
#include "esp_err.h"
|
||||
@@ -242,6 +243,10 @@ static esp_err_t apply_candidate(wifi_app_config_t *candidate)
|
||||
|
||||
static esp_err_t read_secret_no_echo(uint8_t *secret, uint8_t *secret_len)
|
||||
{
|
||||
if (admin_ssh_console_dispatch_is_remote()) {
|
||||
printf("Interactive secret entry is restricted to physical UART0.\n");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY];
|
||||
size_t length = 0U;
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
Reference in New Issue
Block a user