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:
@@ -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)
|
||||
|
||||
+8
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user