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.
This commit is contained in:
2026-08-30 12:02:44 +02:00
parent b7999043ca
commit c7d0d59f3e
5 changed files with 143 additions and 2 deletions
+1
View File
@@ -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)
+7
View File
@@ -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) {
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);
+1 -1
View File
@@ -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;
}
}
+1
View File
@@ -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,
+132
View File
@@ -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, &current) == 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) {