Add Phase 9C security hardening
Generate exact-hash SDK source overrides without modifying dependencies. Harden SSH allocation and algorithm policy, tighten web authentication cleanup, and add focused host contract tests and documentation.
This commit is contained in:
@@ -26,6 +26,8 @@ idf_component_register(
|
||||
"admin_ssh_console.c"
|
||||
"ssh_transport.c"
|
||||
"ssh_auth_policy.c"
|
||||
"ssh_memory.c"
|
||||
"ssh_protocol_policy.c"
|
||||
"ssh_console.c"
|
||||
"usb_cdc_transport.c"
|
||||
"usb_console.c"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "ssh_memory.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_idf_version.h"
|
||||
#include "secure_random.h"
|
||||
|
||||
#if !defined(CONFIG_HEAP_POISONING_DISABLED) || !CONFIG_HEAP_POISONING_DISABLED || \
|
||||
(defined(CONFIG_HEAP_POISONING_LIGHT) && CONFIG_HEAP_POISONING_LIGHT) || \
|
||||
(defined(CONFIG_HEAP_POISONING_COMPREHENSIVE) && CONFIG_HEAP_POISONING_COMPREHENSIVE)
|
||||
#error "SSH memory requires heap poisoning disabled"
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION != ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
#error "Reaudit SSH memory usable extent contract for this IDF"
|
||||
#endif
|
||||
|
||||
void *ssh_memory_malloc(size_t size)
|
||||
{
|
||||
return heap_caps_malloc_prefer(size, 2,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
}
|
||||
|
||||
void ssh_memory_free(void *pointer)
|
||||
{
|
||||
if (pointer == NULL) {
|
||||
return;
|
||||
}
|
||||
secure_wipe(pointer, heap_caps_get_allocated_size(pointer));
|
||||
heap_caps_free(pointer);
|
||||
}
|
||||
|
||||
void *ssh_memory_realloc(void *pointer, size_t size)
|
||||
{
|
||||
if (pointer == NULL) {
|
||||
return ssh_memory_malloc(size);
|
||||
}
|
||||
if (size == 0U) {
|
||||
ssh_memory_free(pointer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Audited unpoisoned IDF 5.5.0 reports the owned usable extent, including
|
||||
* rounding. Do not substitute an interior-pointer/block-containing query. */
|
||||
size_t capacity = heap_caps_get_allocated_size(pointer);
|
||||
if (size <= capacity) {
|
||||
secure_wipe((unsigned char *)pointer + size, capacity - size);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
void *replacement = ssh_memory_malloc(size);
|
||||
if (replacement == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(replacement, pointer, capacity);
|
||||
ssh_memory_free(pointer);
|
||||
return replacement;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Install these hooks before wolfSSH/wolfSSL allocations begin. No module state,
|
||||
* locks or allocation headers. Free/realloc accept only live allocation BASE
|
||||
* pointers from this allocator (or NULL), never interior pointers.
|
||||
*
|
||||
* PSRAM is preferred, with internal 8-bit heap fallback. malloc(0) follows the
|
||||
* SDK allocator. free wipes the full owned usable extent. realloc(NULL, size)
|
||||
* delegates to malloc; realloc(non-NULL, 0) securely frees and returns NULL.
|
||||
*
|
||||
* Realloc within the usable extent retains the pointer AND capacity: it wipes
|
||||
* [size, capacity), but does not reclaim memory. Growth copies the entire old
|
||||
* usable extent, not merely the original requested size. Both allocations are
|
||||
* live until copying finishes; failure leaves the old allocation unchanged.
|
||||
* This is heap-retirement cleanup, not a guarantee about live library buffers
|
||||
* or stack secrets. Only the guarded, audited unpoisoned IDF build is supported.
|
||||
*/
|
||||
void *ssh_memory_malloc(size_t size);
|
||||
void ssh_memory_free(void *pointer);
|
||||
void *ssh_memory_realloc(void *pointer, size_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "ssh_protocol_policy.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <wolfssh/error.h>
|
||||
#include <wolfssh/version.h>
|
||||
|
||||
#if LIBWOLFSSH_VERSION_HEX != 0x01004020
|
||||
#error "Re-audit SSH protocol policy and algorithm-list contracts for this wolfSSH version"
|
||||
#endif
|
||||
|
||||
/* wolfSSH borrows these pointers in both contexts and sessions. */
|
||||
static const char s_kex[] = "curve25519-sha256,ecdh-sha2-nistp256";
|
||||
static const char s_host_key[] = "ecdsa-sha2-nistp256";
|
||||
static const char s_cipher[] = "aes128-gcm@openssh.com,aes256-gcm@openssh.com";
|
||||
static const char s_mac[] = "hmac-sha2-256";
|
||||
/* server-sig-algs advertisement only; the user database enforces enrollment. */
|
||||
static const char s_key_accepted[] = "ssh-ed25519,ecdsa-sha2-nistp256";
|
||||
|
||||
int ssh_protocol_policy_apply(WOLFSSH_CTX *context)
|
||||
{
|
||||
if (context == NULL) {
|
||||
return WS_SSH_CTX_NULL_E;
|
||||
}
|
||||
|
||||
int result = wolfSSH_CTX_SetAlgoListKex(context, s_kex);
|
||||
if (result != WS_SUCCESS) return result;
|
||||
result = wolfSSH_CTX_SetAlgoListKey(context, s_host_key);
|
||||
if (result != WS_SUCCESS) return result;
|
||||
result = wolfSSH_CTX_SetAlgoListCipher(context, s_cipher);
|
||||
if (result != WS_SUCCESS) return result;
|
||||
result = wolfSSH_CTX_SetAlgoListMac(context, s_mac);
|
||||
if (result != WS_SUCCESS) return result;
|
||||
result = wolfSSH_CTX_SetAlgoListKeyAccepted(context, s_key_accepted);
|
||||
if (result != WS_SUCCESS) return result;
|
||||
return WS_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#pragma once
|
||||
|
||||
#include <wolfssh/ssh.h>
|
||||
|
||||
/* Apply before publishing the context or creating sessions. Returns WS_SUCCESS
|
||||
* or the first setter error (WS_SSH_CTX_NULL_E for NULL). Failure may leave a
|
||||
* partially configured context: the caller must discard it, never fall back to
|
||||
* defaults. Does not allocate, free, or publish the context. */
|
||||
int ssh_protocol_policy_apply(WOLFSSH_CTX *context);
|
||||
+10
-20
@@ -10,7 +10,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "admin_ssh_console.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "serial_service.h"
|
||||
#include "ssh_security.h"
|
||||
#include "ssh_auth_policy.h"
|
||||
#include "ssh_memory.h"
|
||||
#include "ssh_protocol_policy.h"
|
||||
#include "user_database.h"
|
||||
#include <wolfssl/wolfcrypt/memory.h>
|
||||
#include <wolfssl/wolfcrypt/random.h>
|
||||
@@ -323,24 +325,6 @@ static bool consume_external_close(const ssh_slot_t *slot, size_t slot_index)
|
||||
return requested;
|
||||
}
|
||||
|
||||
static void *ssh_malloc(size_t size)
|
||||
{
|
||||
return heap_caps_malloc_prefer(size, 2,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
}
|
||||
|
||||
static void ssh_free(void *pointer)
|
||||
{
|
||||
heap_caps_free(pointer);
|
||||
}
|
||||
|
||||
static void *ssh_realloc(void *pointer, size_t size)
|
||||
{
|
||||
return heap_caps_realloc_prefer(pointer, size, 2,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
}
|
||||
|
||||
static int ssh_seed(OS_Seed *seed, byte *output, word32 size)
|
||||
{
|
||||
@@ -741,6 +725,12 @@ static esp_err_t create_context(void)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
result = ssh_protocol_policy_apply(context);
|
||||
if (result != WS_SUCCESS) {
|
||||
wolfSSH_CTX_free(context);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
wolfSSH_SetIORecv(context, bounded_ssh_receive);
|
||||
wolfSSH_SetUserAuth(context, authenticate_user);
|
||||
wolfSSH_SetUserAuthTypes(context, allowed_auth_types);
|
||||
@@ -1540,7 +1530,7 @@ esp_err_t ssh_transport_init(void)
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
goto fail;
|
||||
}
|
||||
if (wolfSSL_SetAllocators(ssh_malloc, ssh_free, ssh_realloc) != 0) {
|
||||
if (wolfSSL_SetAllocators(ssh_memory_malloc, ssh_memory_free, ssh_memory_realloc) != 0) {
|
||||
error = ESP_FAIL;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
+36
-19
@@ -115,6 +115,26 @@ static esp_err_t failure(httpd_req_t *r, const char *status, const char *code)
|
||||
return response(r, status, body);
|
||||
}
|
||||
|
||||
/* An early probe never charges or advances the verification window. Only the
|
||||
* post-parse reservation commits it; retry == 0 means budget is available. */
|
||||
static bool verification_budget(uint64_t epoch, bool reserve, int64_t *retry)
|
||||
{
|
||||
int64_t now = esp_timer_get_time();
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool ready = s_ready && epoch == s_epoch;
|
||||
int64_t window = s_window;
|
||||
unsigned attempts = s_attempts;
|
||||
if (now - window >= WINDOW_US) { window = now; attempts = 0; }
|
||||
*retry = attempts < 5 ? 0 : (window + WINDOW_US - now + 999999LL) / 1000000LL;
|
||||
if (ready && reserve && attempts < 5) {
|
||||
s_window = window;
|
||||
s_attempts = attempts + 1;
|
||||
++s_counts.login_attempts;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return ready;
|
||||
}
|
||||
|
||||
static esp_err_t capacity(httpd_req_t *r)
|
||||
{
|
||||
if (httpd_resp_set_hdr(r, "Retry-After", "5") != ESP_OK) return ESP_FAIL;
|
||||
@@ -248,7 +268,8 @@ esp_err_t web_cookie_auth_handler(httpd_req_t *r)
|
||||
bool document = !strcmp(r->uri, "/login");
|
||||
web_session_view_t view = {0};
|
||||
char canonical[129] = {0}, token[65] = {0}, csrf[65] = {0};
|
||||
char set_cookie[180] = {0}, body[513] = {0};
|
||||
char set_cookie[180] = {0}, body[513] = {0}, retry_after[16] = {0};
|
||||
int64_t retry = 0;
|
||||
web_auth_credentials_t credentials = {0};
|
||||
challenge_t candidate = {0};
|
||||
uint8_t digest[32] = {0}, origin_digest[32] = {0};
|
||||
@@ -361,6 +382,10 @@ esp_err_t web_cookie_auth_handler(httpd_req_t *r)
|
||||
}
|
||||
if (!consumed) { status = "403 Forbidden"; code = "challenge_expired"; goto deny; }
|
||||
if (httpd_resp_set_hdr(r, "Set-Cookie", PRELOGIN_COOKIE "=" COOKIE_FLAGS "0") != ESP_OK) goto cleanup;
|
||||
if (!verification_budget(epoch, false, &retry)) {
|
||||
status = "503 Service Unavailable"; code = "unavailable"; goto deny;
|
||||
}
|
||||
if (retry) goto throttled;
|
||||
size_t received = 0;
|
||||
int64_t deadline = now + 3000000LL;
|
||||
while (received < r->content_len && esp_timer_get_time() < deadline) {
|
||||
@@ -369,29 +394,15 @@ esp_err_t web_cookie_auth_handler(httpd_req_t *r)
|
||||
received += (size_t)count;
|
||||
}
|
||||
if (received != r->content_len || !web_auth_parse_login(body, received, &credentials)) goto deny;
|
||||
now = esp_timer_get_time();
|
||||
unsigned attempts;
|
||||
int64_t retry;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
ready = s_ready && epoch == s_epoch;
|
||||
if (now - s_window >= WINDOW_US) { s_window = now; s_attempts = 0; }
|
||||
attempts = s_attempts;
|
||||
if (ready && attempts < 5) { ++s_attempts; ++s_counts.login_attempts; }
|
||||
retry = (s_window + WINDOW_US - now + 999999LL) / 1000000LL;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!ready) { status = "503 Service Unavailable"; code = "unavailable"; goto deny; }
|
||||
if (attempts >= 5) {
|
||||
char seconds[16];
|
||||
snprintf(seconds, sizeof(seconds), "%lld", (long long)retry);
|
||||
if (httpd_resp_set_hdr(r, "Retry-After", seconds) != ESP_OK) goto cleanup;
|
||||
result = failure(r, "429 Too Many Requests", "throttled");
|
||||
goto cleanup;
|
||||
secure_wipe(body, sizeof(body));
|
||||
if (!verification_budget(epoch, true, &retry)) {
|
||||
status = "503 Service Unavailable"; code = "unavailable"; goto deny;
|
||||
}
|
||||
if (retry) goto throttled;
|
||||
bool authenticated = false;
|
||||
user_principal_t principal = {0};
|
||||
esp_err_t error = user_database_authenticate_password(credentials.username, credentials.username_length,
|
||||
credentials.password, credentials.password_length, &principal, &authenticated);
|
||||
secure_wipe(body, sizeof(body));
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
if (error == ESP_OK && authenticated)
|
||||
error = web_session_store_issue(&principal, canonical, strlen(canonical), token, &view);
|
||||
@@ -406,7 +417,13 @@ esp_err_t web_cookie_auth_handler(httpd_req_t *r)
|
||||
result = response(r, "200 OK", "{\"authenticated\":true}");
|
||||
if (result != ESP_OK) web_session_store_invalidate(view.id);
|
||||
goto cleanup;
|
||||
throttled:
|
||||
status = "429 Too Many Requests"; code = "throttled";
|
||||
snprintf(retry_after, sizeof(retry_after), "%lld", (long long)retry);
|
||||
if (httpd_resp_set_hdr(r, "Retry-After", retry_after) != ESP_OK) goto cleanup;
|
||||
deny:
|
||||
secure_wipe(body, sizeof(body));
|
||||
secure_wipe(&credentials, sizeof(credentials));
|
||||
result = failure(r, status, code);
|
||||
cleanup:
|
||||
if (challenge_published && result != ESP_OK) {
|
||||
|
||||
Reference in New Issue
Block a user