Implement role-aware HTTPS and SSH authentication

This commit is contained in:
2026-08-30 01:31:05 +02:00
parent cd235445c7
commit 0c058b6a8f
16 changed files with 707 additions and 331 deletions
+241 -68
View File
@@ -14,7 +14,6 @@
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
#include "web_security.h"
#if !defined(CONFIG_HTTPD_WS_SUPPORT) || !CONFIG_HTTPD_WS_SUPPORT
#error "web_serial_transport requires CONFIG_HTTPD_WS_SUPPORT"
@@ -30,6 +29,7 @@
#define WEB_SERIAL_TASK_PRIORITY 4U
#define WEB_SERIAL_ACTIVE_BURST_LOOPS 8U
#define WEB_SERIAL_POLL_MS 5U
#define WEB_SERIAL_CURRENTNESS_INTERVAL_US 250000LL
#define WEB_SERIAL_DETACH_TIMEOUT_US 1000000LL
_Static_assert(WEB_SERIAL_TRANSPORT_TICKET_LENGTH ==
@@ -48,7 +48,7 @@ typedef enum {
typedef struct {
uint8_t digest[WEB_SERIAL_SHA256_BYTES];
int64_t expires_at_us;
uint32_t material_generation;
user_principal_t principal;
bool active;
} web_serial_ticket_t;
@@ -67,6 +67,8 @@ typedef struct web_serial_slot {
int socket_fd;
uint32_t generation;
session_broker_client_id_t broker_client_id;
user_principal_t principal;
int64_t next_currentness_check_us;
bool writer;
bool hello_pending;
bool work_pending;
@@ -142,8 +144,8 @@ static bool slot_pointer_valid(const web_serial_slot_t *slot)
static void clear_ticket_locked(web_serial_ticket_t *ticket)
{
secure_wipe(ticket->digest, sizeof(ticket->digest));
secure_wipe(&ticket->principal, sizeof(ticket->principal));
ticket->expires_at_us = 0;
ticket->material_generation = 0U;
ticket->active = false;
}
@@ -154,20 +156,13 @@ static void clear_all_tickets_locked(void)
}
}
static void purge_tickets_locked(int64_t now_us, uint32_t material_generation,
bool check_generation)
static void purge_tickets_locked(int64_t now_us)
{
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (!ticket->active) {
continue;
}
if (ticket->expires_at_us <= now_us) {
if (ticket->active && ticket->expires_at_us <= now_us) {
clear_ticket_locked(ticket);
++s_counters.tickets_expired;
} else if (check_generation &&
ticket->material_generation != material_generation) {
clear_ticket_locked(ticket);
}
}
}
@@ -209,16 +204,6 @@ static bool base64url_character(char value)
(value >= '0' && value <= '9') || value == '-' || value == '_';
}
static esp_err_t current_material_generation(uint32_t *generation)
{
web_security_certificate_metadata_t metadata;
esp_err_t result = web_security_get_certificate_metadata(&metadata);
if (result == ESP_OK) {
*generation = metadata.material_generation;
}
secure_wipe(&metadata, sizeof(metadata));
return result;
}
static esp_err_t sha256_ticket(const char *ticket,
uint8_t digest[WEB_SERIAL_SHA256_BYTES])
@@ -295,16 +280,16 @@ static esp_err_t validate_origin(httpd_req_t *request)
return matches ? ESP_OK : ESP_ERR_INVALID_ARG;
}
static esp_err_t consume_ticket(const char *ticket, bool *consumed)
static esp_err_t consume_ticket(const char *ticket,
user_principal_t *principal, bool *consumed)
{
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
user_principal_t candidate = {0};
bool ticket_found = false;
*consumed = false;
memset(principal, 0, sizeof(*principal));
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = sha256_ticket(ticket, digest);
}
esp_err_t result = sha256_ticket(ticket, digest);
if (result != ESP_OK) {
secure_wipe(digest, sizeof(digest));
add_counter(&s_counters.tickets_rejected, 1U);
@@ -326,36 +311,45 @@ static esp_err_t consume_ticket(const char *ticket, bool *consumed)
}
}
purge_tickets_locked(now_us, generation, true);
purge_tickets_locked(now_us);
if (matching_count == 1U &&
matching_index < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[matching_index];
if (entry->active && entry->expires_at_us > now_us &&
entry->material_generation == generation) {
if (entry->active && entry->expires_at_us > now_us) {
candidate = entry->principal;
clear_ticket_locked(entry);
++s_counters.tickets_consumed;
*consumed = true;
ticket_found = true;
}
}
if (!*consumed) {
if (!ticket_found && matching_count > 1U) {
/* A defensive duplicate can never become a repeatedly usable ticket. */
if (matching_count > 1U) {
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (entry->active && constant_time_equal(
entry->digest, digest,
sizeof(digest))) {
clear_ticket_locked(entry);
}
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (entry->active && constant_time_equal(
entry->digest, digest, sizeof(digest))) {
clear_ticket_locked(entry);
}
}
++s_counters.tickets_rejected;
}
taskEXIT_CRITICAL(&s_lock);
if (ticket_found) {
bool current = false;
result = user_database_principal_is_current(&candidate, &current);
if (result == ESP_OK && current) {
*principal = candidate;
*consumed = true;
add_counter(&s_counters.tickets_consumed, 1U);
}
}
if (!*consumed) {
add_counter(&s_counters.tickets_rejected, 1U);
}
secure_wipe(&candidate, sizeof(candidate));
secure_wipe(digest, sizeof(digest));
return ESP_OK;
return result;
}
static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
@@ -376,6 +370,8 @@ static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
slot->server = server;
slot->socket_fd = socket_fd;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -399,6 +395,8 @@ static void make_slot_free_locked(web_serial_slot_t *slot)
slot->server = NULL;
slot->socket_fd = -1;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -481,6 +479,8 @@ static void web_serial_session_free(void *context)
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
@@ -517,6 +517,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
uint32_t slot_generation = 0U;
web_serial_slot_t *slot = NULL;
user_principal_t principal = {0};
bool consumed = false;
esp_err_t result;
@@ -534,12 +535,25 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
result = consume_ticket(ticket, &consumed);
result = consume_ticket(ticket, &principal, &consumed);
if (result != ESP_OK || !consumed) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
taskENTER_CRITICAL(&s_lock);
bool principal_staged = slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
!slot->close_requested;
if (principal_staged) {
slot->principal = principal;
}
taskEXIT_CRITICAL(&s_lock);
if (!principal_staged) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
if (!serial_service_is_running()) {
result = serial_service_start();
@@ -556,6 +570,14 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
}
}
bool principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
size_t slot_index = (size_t)(slot - s_slots);
char client_name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
int written = snprintf(client_name, sizeof(client_name),
@@ -579,6 +601,14 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
add_counter(&s_counters.writer_requests, 1U);
result = session_broker_request_writer(client_id);
bool writer = result == ESP_OK;
@@ -594,13 +624,25 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
bool activated = false;
int64_t next_currentness_check_us =
monotonic_time_us() + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
slot->generation == slot_generation && !slot->close_requested &&
s_server == request->handle) {
slot->state = WEB_SERIAL_SLOT_ACTIVE;
slot->broker_client_id = client_id;
slot->principal = principal;
slot->next_currentness_check_us = next_currentness_check_us;
slot->writer = writer;
slot->hello_pending = true;
++s_counters.connections;
@@ -622,13 +664,15 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
result = ESP_OK;
cleanup:
secure_wipe(&principal, sizeof(principal));
secure_wipe(ticket, sizeof(ticket));
return result;
}
static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slot_out,
uint32_t *generation,
session_broker_client_id_t *client_id)
session_broker_client_id_t *client_id,
user_principal_t *principal)
{
web_serial_slot_t *slot = request->sess_ctx;
int socket_fd = httpd_req_to_sockfd(request);
@@ -647,6 +691,7 @@ static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slo
*slot_out = slot;
*generation = slot->generation;
*client_id = slot->broker_client_id;
*principal = slot->principal;
}
taskEXIT_CRITICAL(&s_lock);
return valid;
@@ -757,7 +802,9 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
web_serial_slot_t *slot = NULL;
uint32_t generation = 0U;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
if (!capture_active_session(request, &slot, &generation, &client_id)) {
user_principal_t principal = {0};
if (!capture_active_session(request, &slot, &generation, &client_id,
&principal)) {
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
@@ -765,27 +812,52 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
int socket_fd = httpd_req_to_sockfd(request);
if (httpd_ws_get_fd_info(request->handle, socket_fd) !=
HTTPD_WS_CLIENT_WEBSOCKET) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, 0U);
}
httpd_ws_frame_t frame = {0};
esp_err_t result = httpd_ws_recv_frame(request, &frame, 0U);
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
if (!frame.final || frame.type == HTTPD_WS_TYPE_CONTINUE ||
frame.len > WEB_SERIAL_TRANSPORT_MAX_RX_PAYLOAD ||
(frame.type != HTTPD_WS_TYPE_BINARY &&
frame.type != HTTPD_WS_TYPE_TEXT)) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
frame.payload = slot->rx_data;
result = httpd_ws_recv_frame(request, &frame, sizeof(slot->rx_data));
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
return reject_protocol_frame(slot, generation, frame.len);
}
bool current = false;
esp_err_t currentness_result =
user_database_principal_is_current(&principal, &current);
secure_wipe(&principal, sizeof(principal));
if (currentness_result != ESP_OK || !current) {
request_handler_close(slot, generation);
return ESP_FAIL;
}
taskENTER_CRITICAL(&s_lock);
bool still_active = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->close_requested &&
slot->generation == generation &&
slot->broker_client_id == client_id &&
slot->server == request->handle &&
slot->server == s_server;
taskEXIT_CRITICAL(&s_lock);
if (!still_active) {
return ESP_FAIL;
}
if (frame.type == HTTPD_WS_TYPE_BINARY) {
return process_binary_frame(slot, generation, client_id,
slot->rx_data, frame.len);
@@ -817,19 +889,31 @@ static void web_serial_send_work(void *argument)
httpd_ws_type_t type;
size_t length;
bool valid;
bool retired = false;
taskENTER_CRITICAL(&s_lock);
valid = slot->work_pending && work == &slot->work &&
work->generation == slot->generation &&
work->server == slot->server &&
work->socket_fd == slot->socket_fd;
bool owned_work = slot->work_pending && work == &slot->work &&
work->generation == slot->generation &&
work->server == slot->server &&
work->socket_fd == slot->socket_fd;
valid = owned_work && slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->close_requested;
server = work->server;
socket_fd = work->socket_fd;
generation = work->generation;
type = slot->tx_type;
length = slot->tx_length;
if (owned_work && !valid) {
slot->work_pending = false;
slot->tx_length = 0U;
finish_closing_slot_locked(slot);
retired = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!valid) {
if (retired) {
notify_transport_task();
}
return;
}
@@ -1113,6 +1197,8 @@ static void process_close_request(web_serial_slot_t *slot)
++s_counters.queue_failures;
if (session_gone) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
@@ -1164,6 +1250,42 @@ static void process_broker_disconnect(web_serial_slot_t *slot)
taskEXIT_CRITICAL(&s_lock);
}
static void process_principal_currentness(web_serial_slot_t *slot)
{
user_principal_t principal = {0};
uint32_t generation = 0U;
bool check = false;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE && !slot->close_requested &&
slot->next_currentness_check_us <= now_us) {
generation = slot->generation;
principal = slot->principal;
slot->next_currentness_check_us =
now_us + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
check = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!check) {
return;
}
bool current = false;
esp_err_t result = user_database_principal_is_current(&principal, &current);
secure_wipe(&principal, sizeof(principal));
if (result == ESP_OK && current) {
return;
}
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
}
static void process_active_output(web_serial_slot_t *slot)
{
uint32_t generation;
@@ -1221,6 +1343,7 @@ static void transport_task(void *context)
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
web_serial_slot_t *slot = &s_slots[index];
process_principal_currentness(slot);
process_close_request(slot);
process_broker_disconnect(slot);
process_active_output(slot);
@@ -1332,6 +1455,8 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
slot->tx_length = 0U;
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
@@ -1398,20 +1523,27 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
return result;
}
esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
char *ticket, size_t capacity)
{
if (ticket == NULL || capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
if (principal == NULL || ticket == NULL ||
capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
return ESP_ERR_INVALID_ARG;
}
ticket[0] = '\0';
bool current = false;
esp_err_t result = user_database_principal_is_current(principal, &current);
if (result != ESP_OK) {
return result;
}
if (!current) {
return ESP_ERR_INVALID_STATE;
}
uint8_t random_bytes[WEB_SERIAL_RANDOM_BYTES] = {0};
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = secure_random_fill(random_bytes, sizeof(random_bytes));
}
result = secure_random_fill(random_bytes, sizeof(random_bytes));
if (result == ESP_OK) {
encode_base64url_24(random_bytes, ticket);
result = sha256_ticket(ticket, digest);
@@ -1425,7 +1557,7 @@ esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
bool stored = false;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && s_server != NULL) {
purge_tickets_locked(now_us, generation, true);
purge_tickets_locked(now_us);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
@@ -1447,7 +1579,7 @@ esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
entry->expires_at_us =
now_us + (int64_t)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS *
1000000LL;
entry->material_generation = generation;
entry->principal = *principal;
entry->active = true;
++s_counters.tickets_issued;
stored = true;
@@ -1467,9 +1599,9 @@ cleanup:
}
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request)
httpd_req_t *request, const user_principal_t *principal)
{
if (request == NULL) {
if (request == NULL || principal == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (request->method != HTTP_POST || request->content_len != 0U ||
@@ -1487,7 +1619,8 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
char response[WEB_SERIAL_TICKET_RESPONSE_CAPACITY];
esp_err_t result = web_serial_transport_mint_ticket(ticket, sizeof(ticket));
esp_err_t result = web_serial_transport_mint_ticket(
principal, ticket, sizeof(ticket));
if (result != ESP_OK) {
secure_wipe(ticket, sizeof(ticket));
return result;
@@ -1573,8 +1706,6 @@ esp_err_t web_serial_transport_get_snapshot(
return ESP_ERR_INVALID_ARG;
}
uint32_t generation = 0U;
bool have_generation = current_material_generation(&generation) == ESP_OK;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
@@ -1582,7 +1713,7 @@ esp_err_t web_serial_transport_get_snapshot(
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
purge_tickets_locked(now_us, generation, have_generation);
purge_tickets_locked(now_us);
memset(snapshot, 0, sizeof(*snapshot));
snapshot->initialized = true;
snapshot->server_attached = s_server != NULL;
@@ -1602,12 +1733,17 @@ esp_err_t web_serial_transport_get_snapshot(
continue;
}
session->active = true;
session->principal_valid = true;
session->writer = slot->writer;
session->tx_pending = slot->work_pending;
session->close_requested = slot->close_requested;
session->socket_fd = slot->socket_fd;
session->generation = slot->generation;
session->broker_client_id = slot->broker_client_id;
session->user_role = slot->principal.role;
session->auth_method = slot->principal.method;
memcpy(session->username, slot->principal.username,
slot->principal.username_length);
++snapshot->active_sessions;
}
taskEXIT_CRITICAL(&s_lock);
@@ -1626,6 +1762,43 @@ esp_err_t web_serial_transport_clear_counters(void)
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
size_t username_length)
{
if (!user_database_username_valid(username, username_length)) {
return ESP_ERR_INVALID_ARG;
}
bool notify = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (ticket->active && ticket->principal.username_length == username_length &&
memcmp(ticket->principal.username, username, username_length) == 0) {
clear_ticket_locked(ticket);
}
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
web_serial_slot_t *slot = &s_slots[index];
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
slot->state == WEB_SERIAL_SLOT_ACTIVE) &&
slot->principal.username_length == username_length &&
memcmp(slot->principal.username, username, username_length) == 0) {
slot->close_requested = true;
notify = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (notify) {
notify_transport_task();
}
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_sessions(void)
{
taskENTER_CRITICAL(&s_lock);