From c7d0d59f3e91077f0ed20353b593e3f9fea7f4ea Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 30 Aug 2026 12:02:44 +0200 Subject: [PATCH] Cache web authentication results Add a short-lived, HMAC-keyed cache for validated principals and invalidate entries when principals become stale. Improve duplicate SSH key errors and enable Ed25519 streaming verification. --- CMakeLists.txt | 1 + src/user_console.c | 9 ++- src/user_database.c | 2 +- src/user_database.h | 1 + src/web_server.c | 132 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cac74d3..b94ecb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ add_compile_definitions( WC_RNG_SEED_CB NO_WOLFSSL_ESP32_CRYPT_AES NO_WOLFSSL_ESP32_CRYPT_HASH + WOLFSSL_ED25519_STREAMING_VERIFY ) include($ENV{IDF_PATH}/tools/cmake/project.cmake) diff --git a/src/user_console.c b/src/user_console.c index e9eed41..41dfb8a 100644 --- a/src/user_console.c +++ b/src/user_console.c @@ -356,7 +356,14 @@ static int add_key(const char *username) secure_wipe(blob, sizeof(blob)); secure_wipe(line, sizeof(line)); if (error != ESP_OK) { - printf("Could not add SSH key: %s\n", esp_err_to_name(error)); + if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) { + printf("Could not add SSH key: that public key is already assigned to an 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); diff --git a/src/user_database.c b/src/user_database.c index c443b66..d6096ac 100644 --- a/src/user_database.c +++ b/src/user_database.c @@ -1175,7 +1175,7 @@ esp_err_t user_database_add_ssh_key( key->blob_length == key_blob_length && memcmp(key->type, key_type, key_type_length) == 0 && constant_time_equal(key->blob, key_blob, key_blob_length)) { - error = ESP_ERR_INVALID_STATE; + error = USER_DATABASE_ERR_DUPLICATE_SSH_KEY; break; } } diff --git a/src/user_database.h b/src/user_database.h index 3d6e291..15f82f4 100644 --- a/src/user_database.h +++ b/src/user_database.h @@ -24,6 +24,7 @@ extern "C" { #define USER_DATABASE_SSH_KEY_TYPE_CAPACITY 32U #define USER_DATABASE_SSH_KEY_BLOB_CAPACITY 128U #define USER_DATABASE_SHA256_LENGTH 32U +#define USER_DATABASE_ERR_DUPLICATE_SSH_KEY ESP_ERR_NOT_ALLOWED typedef enum { USER_ROLE_USER = 1, diff --git a/src/web_server.c b/src/web_server.c index 5e8d525..87853e6 100644 --- a/src/web_server.c +++ b/src/web_server.c @@ -15,6 +15,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "mbedtls/base64.h" +#include "mbedtls/md.h" #include "secure_random.h" #include "serial_config.h" #include "serial_service.h" @@ -34,6 +35,17 @@ #define WEB_SERVER_MAX_AUTHORIZATION \ ((sizeof("Basic ") - 1U) + WEB_SERVER_MAX_BASIC_ENCODED + 1U) #define WEB_SERVER_STATUS_JSON_CAPACITY 3072U +#define WEB_SERVER_AUTH_CACHE_ENTRIES 4U +#define WEB_SERVER_AUTH_CACHE_KEY_LENGTH 32U +#define WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH 32U +#define WEB_SERVER_AUTH_CACHE_TTL_US 300000000LL + +typedef struct { + bool active; + int64_t expires_at_us; + uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH]; + user_principal_t principal; +} web_server_auth_cache_entry_t; static SemaphoreHandle_t s_server_mutex; static httpd_handle_t s_server; @@ -45,6 +57,10 @@ static bool s_serial_transport_attached; static esp_err_t s_last_error = ESP_ERR_INVALID_STATE; static esp_err_t s_serial_transport_error = ESP_ERR_INVALID_STATE; static web_server_counters_t s_counters; +static bool s_auth_cache_ready; +static uint8_t s_auth_cache_key[WEB_SERVER_AUTH_CACHE_KEY_LENGTH]; +static web_server_auth_cache_entry_t + s_auth_cache[WEB_SERVER_AUTH_CACHE_ENTRIES]; static esp_err_t ensure_mutex(void) { @@ -106,6 +122,93 @@ static esp_err_t send_authentication_required(httpd_req_t *request) return send_plain_error(request, "401 Unauthorized", "Authentication required.\n"); } +static bool constant_time_equal(const uint8_t *left, const uint8_t *right, + size_t length) +{ + uint8_t difference = 0U; + for (size_t index = 0U; index < length; ++index) { + difference |= left[index] ^ right[index]; + } + return difference == 0U; +} + +static esp_err_t calculate_auth_cache_digest( + const char *authorization, size_t authorization_length, + uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH]) +{ + if (!s_auth_cache_ready) { + return ESP_ERR_INVALID_STATE; + } + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (info == NULL || + mbedtls_md_hmac(info, s_auth_cache_key, sizeof(s_auth_cache_key), + (const uint8_t *)authorization, authorization_length, + digest) != 0) { + return ESP_FAIL; + } + return ESP_OK; +} + +static bool authenticate_from_cache( + const uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH], + user_principal_t *principal) +{ + int64_t now = esp_timer_get_time(); + for (size_t index = 0U; index < WEB_SERVER_AUTH_CACHE_ENTRIES; ++index) { + web_server_auth_cache_entry_t *entry = &s_auth_cache[index]; + if (!entry->active || entry->expires_at_us <= now || + !constant_time_equal(entry->digest, digest, sizeof(entry->digest))) { + if (entry->active && entry->expires_at_us <= now) { + secure_wipe(entry, sizeof(*entry)); + } + continue; + } + + bool current = false; + if (user_database_principal_is_current(&entry->principal, ¤t) == ESP_OK && + current) { + *principal = entry->principal; + entry->expires_at_us = now + WEB_SERVER_AUTH_CACHE_TTL_US; + return true; + } + secure_wipe(entry, sizeof(*entry)); + return false; + } + return false; +} + +static void store_authenticated_request( + const uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH], + const user_principal_t *principal) +{ + int64_t now = esp_timer_get_time(); + size_t selected = 0U; + int64_t earliest_expiry = INT64_MAX; + for (size_t index = 0U; index < WEB_SERVER_AUTH_CACHE_ENTRIES; ++index) { + web_server_auth_cache_entry_t *entry = &s_auth_cache[index]; + if (entry->active && + constant_time_equal(entry->digest, digest, sizeof(entry->digest))) { + selected = index; + break; + } + if (!entry->active || entry->expires_at_us <= now) { + selected = index; + earliest_expiry = INT64_MIN; + } else if (earliest_expiry != INT64_MIN && + entry->expires_at_us < earliest_expiry) { + selected = index; + earliest_expiry = entry->expires_at_us; + } + } + + web_server_auth_cache_entry_t *entry = &s_auth_cache[selected]; + secure_wipe(entry, sizeof(*entry)); + entry->active = true; + entry->expires_at_us = now + WEB_SERVER_AUTH_CACHE_TTL_US; + memcpy(entry->digest, digest, sizeof(entry->digest)); + entry->principal = *principal; +} + static esp_err_t authenticate_request(httpd_req_t *request, user_principal_t *principal, bool *authenticated) @@ -113,6 +216,8 @@ static esp_err_t authenticate_request(httpd_req_t *request, char authorization[WEB_SERVER_MAX_AUTHORIZATION] = {0}; uint8_t decoded[WEB_SERVER_MAX_BASIC_DECODED] = {0}; size_t decoded_length = 0U; + uint8_t cache_digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH] = {0}; + bool cache_digest_valid = false; esp_err_t result = ESP_OK; memset(principal, 0, sizeof(*principal)); *authenticated = false; @@ -128,6 +233,16 @@ static esp_err_t authenticate_request(httpd_req_t *request, goto cleanup; } + result = calculate_auth_cache_digest(authorization, header_length, cache_digest); + if (result != ESP_OK) { + goto cleanup; + } + cache_digest_valid = true; + if (authenticate_from_cache(cache_digest, principal)) { + *authenticated = true; + goto cleanup; + } + int decode_result = mbedtls_base64_decode( decoded, sizeof(decoded), &decoded_length, (const unsigned char *)authorization + 6U, header_length - 6U); @@ -144,10 +259,14 @@ static esp_err_t authenticate_request(httpd_req_t *request, result = user_database_authenticate_password( decoded, username_length, separator + 1U, password_length, principal, authenticated); + if (result == ESP_OK && *authenticated && cache_digest_valid) { + store_authenticated_request(cache_digest, principal); + } cleanup: secure_wipe(authorization, sizeof(authorization)); secure_wipe(decoded, sizeof(decoded)); + secure_wipe(cache_digest, sizeof(cache_digest)); if (result != ESP_OK) { memset(principal, 0, sizeof(*principal)); return result; @@ -475,6 +594,19 @@ esp_err_t web_server_init(void) return error; } + xSemaphoreTake(s_server_mutex, portMAX_DELAY); + if (!s_auth_cache_ready) { + error = secure_random_fill(s_auth_cache_key, sizeof(s_auth_cache_key)); + if (error == ESP_OK) { + secure_wipe(s_auth_cache, sizeof(s_auth_cache)); + s_auth_cache_ready = true; + } + } + xSemaphoreGive(s_server_mutex); + if (error != ESP_OK) { + return error; + } + bool initialize_serial_transport = false; xSemaphoreTake(s_server_mutex, portMAX_DELAY); if (!s_serial_transport_init_attempted) {