Bind Serial Transports To Web Sessions

This commit is contained in:
2026-09-05 18:01:39 +02:00
parent 93eef0e676
commit a62a655ac1
18 changed files with 528 additions and 35 deletions
+104 -17
View File
@@ -49,6 +49,7 @@ typedef struct {
uint8_t digest[WEB_SERIAL_SHA256_BYTES];
int64_t expires_at_us;
user_principal_t principal;
web_session_id_t web_session_id;
bool active;
} web_serial_ticket_t;
@@ -68,6 +69,7 @@ typedef struct web_serial_slot {
uint32_t generation;
session_broker_client_id_t broker_client_id;
user_principal_t principal;
web_session_id_t web_session_id;
int64_t next_currentness_check_us;
bool writer;
bool hello_pending;
@@ -95,6 +97,16 @@ static web_serial_slot_t s_slots[WEB_SERIAL_TRANSPORT_MAX_SESSIONS];
static web_serial_transport_counters_t s_counters;
static uint32_t s_httpd_close_operations;
static uint32_t s_inflight_handlers;
/* Cancels ticket publication across revocation and server detach/re-attach.
* Never wraps: exhaustion disables minting for the remainder of the boot. */
static uint64_t s_ticket_epoch;
static esp_err_t identity_is_current(const user_principal_t *principal,
web_session_id_t id, bool *current)
{
return id == 0U ? user_database_principal_is_current(principal, current)
: web_session_store_check_principal(id, principal, current);
}
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
@@ -146,6 +158,7 @@ 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->web_session_id = 0U;
ticket->active = false;
}
@@ -281,6 +294,7 @@ static esp_err_t validate_origin(httpd_req_t *request)
}
static esp_err_t consume_ticket(const char *ticket,
web_session_id_t web_session_id,
user_principal_t *principal, bool *consumed)
{
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
@@ -315,7 +329,8 @@ static esp_err_t consume_ticket(const char *ticket,
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) {
if (entry->active && entry->expires_at_us > now_us &&
entry->web_session_id == web_session_id) {
candidate = entry->principal;
clear_ticket_locked(entry);
ticket_found = true;
@@ -336,7 +351,7 @@ static esp_err_t consume_ticket(const char *ticket,
if (ticket_found) {
bool current = false;
result = user_database_principal_is_current(&candidate, &current);
result = identity_is_current(&candidate, web_session_id, &current);
if (result == ESP_OK && current) {
*principal = candidate;
*consumed = true;
@@ -371,6 +386,7 @@ static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
slot->socket_fd = socket_fd;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->web_session_id = 0U;
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -396,6 +412,7 @@ static void make_slot_free_locked(web_serial_slot_t *slot)
slot->socket_fd = -1;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
slot->web_session_id = 0U;
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -512,7 +529,8 @@ static esp_err_t send_plain_bad_request(httpd_req_t *request)
return result;
}
static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd,
web_session_id_t web_session_id)
{
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
uint32_t slot_generation = 0U;
@@ -535,7 +553,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
result = consume_ticket(ticket, &principal, &consumed);
result = consume_ticket(ticket, web_session_id, &principal, &consumed);
if (result != ESP_OK || !consumed) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
@@ -547,6 +565,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
!slot->close_requested;
if (principal_staged) {
slot->principal = principal;
slot->web_session_id = web_session_id;
}
taskEXIT_CRITICAL(&s_lock);
if (!principal_staged) {
@@ -571,7 +590,7 @@ 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);
result = identity_is_current(&principal, web_session_id, &principal_current);
if (result != ESP_OK || !principal_current) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
@@ -602,7 +621,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
result = identity_is_current(&principal, web_session_id, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
@@ -625,7 +644,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
result = identity_is_current(&principal, web_session_id, &principal_current);
if (result != ESP_OK || !principal_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
@@ -672,7 +691,8 @@ cleanup:
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,
user_principal_t *principal)
user_principal_t *principal,
web_session_id_t *web_session_id)
{
web_serial_slot_t *slot = request->sess_ctx;
int socket_fd = httpd_req_to_sockfd(request);
@@ -692,6 +712,7 @@ static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slo
*generation = slot->generation;
*client_id = slot->broker_client_id;
*principal = slot->principal;
*web_session_id = slot->web_session_id;
}
taskEXIT_CRITICAL(&s_lock);
return valid;
@@ -803,8 +824,9 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
uint32_t generation = 0U;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
user_principal_t principal = {0};
web_session_id_t web_session_id = 0U;
if (!capture_active_session(request, &slot, &generation, &client_id,
&principal)) {
&principal, &web_session_id)) {
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
@@ -839,7 +861,7 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
bool current = false;
esp_err_t currentness_result =
user_database_principal_is_current(&principal, &current);
identity_is_current(&principal, web_session_id, &current);
secure_wipe(&principal, sizeof(principal));
if (currentness_result != ESP_OK || !current) {
request_handler_close(slot, generation);
@@ -1253,6 +1275,7 @@ static void process_broker_disconnect(web_serial_slot_t *slot)
static void process_principal_currentness(web_serial_slot_t *slot)
{
user_principal_t principal = {0};
web_session_id_t web_session_id = 0U;
uint32_t generation = 0U;
bool check = false;
int64_t now_us = monotonic_time_us();
@@ -1262,6 +1285,7 @@ static void process_principal_currentness(web_serial_slot_t *slot)
slot->next_currentness_check_us <= now_us) {
generation = slot->generation;
principal = slot->principal;
web_session_id = slot->web_session_id;
slot->next_currentness_check_us =
now_us + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
check = true;
@@ -1272,7 +1296,7 @@ static void process_principal_currentness(web_serial_slot_t *slot)
}
bool current = false;
esp_err_t result = user_database_principal_is_current(&principal, &current);
esp_err_t result = identity_is_current(&principal, web_session_id, &current);
secure_wipe(&principal, sizeof(principal));
if (result == ESP_OK && current) {
return;
@@ -1417,6 +1441,9 @@ esp_err_t web_serial_transport_attach_server(httpd_handle_t server)
} else {
clear_all_tickets_locked();
s_server = server;
if (s_ticket_epoch != UINT64_MAX) {
++s_ticket_epoch;
}
}
taskEXIT_CRITICAL(&s_lock);
if (result == ESP_OK) {
@@ -1443,6 +1470,9 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
* observes the cleared server and disconnects unpublished broker state.
*/
s_server = NULL;
if (s_ticket_epoch != UINT64_MAX) {
++s_ticket_epoch;
}
clear_all_tickets_locked();
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
@@ -1524,6 +1554,7 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
}
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
web_session_id_t web_session_id,
char *ticket, size_t capacity)
{
if (principal == NULL || ticket == NULL ||
@@ -1532,8 +1563,12 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
}
ticket[0] = '\0';
taskENTER_CRITICAL(&s_lock);
uint64_t epoch = s_ticket_epoch;
taskEXIT_CRITICAL(&s_lock);
bool current = false;
esp_err_t result = user_database_principal_is_current(principal, &current);
esp_err_t result = identity_is_current(principal, web_session_id, &current);
if (result != ESP_OK) {
return result;
}
@@ -1553,10 +1588,18 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
goto cleanup;
}
current = false;
result = identity_is_current(principal, web_session_id, &current);
if (result != ESP_OK || !current) {
result = ESP_ERR_INVALID_STATE;
ticket[0] = '\0';
goto cleanup;
}
int64_t now_us = monotonic_time_us();
bool stored = false;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && s_server != NULL) {
if (s_initialized && s_server != NULL && epoch == s_ticket_epoch &&
epoch != UINT64_MAX) {
purge_tickets_locked(now_us);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
@@ -1580,6 +1623,7 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
now_us + (int64_t)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS *
1000000LL;
entry->principal = *principal;
entry->web_session_id = web_session_id;
entry->active = true;
++s_counters.tickets_issued;
stored = true;
@@ -1599,7 +1643,8 @@ cleanup:
}
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request, const user_principal_t *principal)
httpd_req_t *request, const user_principal_t *principal,
web_session_id_t web_session_id)
{
if (request == NULL || principal == NULL) {
return ESP_ERR_INVALID_ARG;
@@ -1620,7 +1665,7 @@ 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(
principal, ticket, sizeof(ticket));
principal, web_session_id, ticket, sizeof(ticket));
if (result != ESP_OK) {
secure_wipe(ticket, sizeof(ticket));
return result;
@@ -1652,6 +1697,12 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
}
esp_err_t web_serial_transport_ws_handler(httpd_req_t *request)
{
return web_serial_transport_session_ws_handler(request, 0U);
}
esp_err_t web_serial_transport_session_ws_handler(httpd_req_t *request,
web_session_id_t web_session_id)
{
if (request == NULL || request->handle == NULL) {
return ESP_ERR_INVALID_ARG;
@@ -1686,7 +1737,7 @@ esp_err_t web_serial_transport_ws_handler(httpd_req_t *request)
esp_err_t result;
if (request->sess_ctx == NULL) {
/* IDF has already sent 101; authentication failures must only close. */
result = connect_websocket(request, socket_fd);
result = connect_websocket(request, socket_fd, web_session_id);
} else {
result = process_websocket_frame(request);
}
@@ -1769,8 +1820,12 @@ esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
return ESP_ERR_INVALID_ARG;
}
web_session_store_invalidate_username(username, username_length);
bool notify = false;
taskENTER_CRITICAL(&s_lock);
if (s_ticket_epoch != UINT64_MAX) {
++s_ticket_epoch;
}
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
@@ -1801,7 +1856,11 @@ esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
esp_err_t web_serial_transport_revoke_sessions(void)
{
web_session_store_invalidate_username(NULL, 0U);
taskENTER_CRITICAL(&s_lock);
if (s_ticket_epoch != UINT64_MAX) {
++s_ticket_epoch;
}
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
@@ -1810,7 +1869,35 @@ esp_err_t web_serial_transport_revoke_sessions(void)
clear_all_tickets_locked();
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_ACTIVE) {
if (slot->state == WEB_SERIAL_SLOT_ACTIVE ||
slot->state == WEB_SERIAL_SLOT_RESERVED) {
slot->close_requested = true;
}
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_web_session(web_session_id_t id)
{
if (id == 0U) {
return ESP_ERR_INVALID_ARG;
}
web_session_store_invalidate(id);
taskENTER_CRITICAL(&s_lock);
if (s_ticket_epoch != UINT64_MAX) {
++s_ticket_epoch;
}
for (size_t i = 0U; i < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++i) {
if (s_tickets[i].active && s_tickets[i].web_session_id == id) {
clear_ticket_locked(&s_tickets[i]);
}
}
for (size_t i = 0U; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
web_serial_slot_t *slot = &s_slots[i];
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
slot->state == WEB_SERIAL_SLOT_ACTIVE) && slot->web_session_id == id) {
slot->close_requested = true;
}
}