Files
ESP32_Serial_Swiss_Army_Knife/tools/security_overrides.py
T
Commander1024 797d2681ac Migrate to IDF 5.5.3 candidate
Pin PlatformIO packages and toolchains, rebase protected SDK
overrides, and add WebSocket receive regression coverage. Document
isolated candidate validation, archive provenance, and remaining gates.
2026-09-18 14:23:13 +02:00

803 lines
32 KiB
Python

#!/usr/bin/env python3
"""Generate audited SDK sources in the build tree, never modify dependencies.
Extend ENTRIES with an independently pinned Entry (root='idf' or 'project').
Every Edit must match exactly once. CMake consumes the generated manifest and
replaces only the matching component source, retaining its compile properties.
"""
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import argparse
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import re
import sys
import tempfile
class OverrideError(ValueError):
pass
@dataclass(frozen=True)
class Edit:
old: str
new: str
@dataclass(frozen=True)
class Entry:
name: str
component: str
root: str
source: str
sha256: str
edits: tuple[Edit, ...]
target: str = "" # Explicit nested library; empty means IDF COMPONENT_LIB.
header: bool = False # PUBLIC overlay, never a translation unit.
MODIFICATION_NOTICE = """/* Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15.
* Generated security-corrected source; edits are maintained in
* tools/security_overrides.py. Do not edit this generated copy.
* Upstream copyright and license notices are retained below.
*/
"""
# Exact, reviewed consolidated delta; upstream mail patches are provenance only.
# No network, patch utility, fuzz, or managed-component mutation at configure time.
WOLFSSH_ORDER_DIR = Path(__file__).resolve().parent / "wolfssh_order"
WOLFSSH_ORDER_PLAN = json.loads((WOLFSSH_ORDER_DIR / "delta.json").read_text())
WOLFSSH_ORDER_EDITS = {
path: tuple(Edit(**edit) for edit in item["edits"])
for path, item in WOLFSSH_ORDER_PLAN.items()
}
WIPE = """/* Retired secret-bearing storage must not survive allocator reuse. */
static void security_override_wipe(void *buffer, size_t length)
{
volatile unsigned char *p = buffer;
while (length--) {
*p++ = 0;
}
}
"""
SCRATCH_RESIZE = """/* Keep shrinking as well as growing: no permanent maximum-size allocation.
* Allocation failure preserves the old buffer for normal request cleanup.
* A resize briefly owns both bounded allocations so the retired copy can wipe. */
static bool security_override_resize_scratch(struct httpd_req_aux *ra, size_t size)
{
if (size == ra->scratch_cur_size) {
return true;
}
char *replacement = malloc(size);
if (replacement == NULL) {
return false;
}
if (ra->scratch != NULL) {
memcpy(replacement, ra->scratch, MIN(size, ra->scratch_cur_size));
security_override_wipe(ra->scratch, ra->scratch_cur_size);
free(ra->scratch);
}
ra->scratch = replacement;
ra->scratch_cur_size = size;
return true;
}
"""
TLS_GUARDS = """/* The server profile and record-buffer wipe audit require these features.
* Client cipher configuration is deliberately unchanged. */
#if !defined(MBEDTLS_SSL_PROTO_TLS1_2) || !defined(MBEDTLS_SSL_SRV_C) || \\
!defined(MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED) || \\
!defined(MBEDTLS_ECDH_C) || !defined(MBEDTLS_ECDSA_C) || \\
!defined(MBEDTLS_AES_C) || !defined(MBEDTLS_GCM_C) || \\
!defined(MBEDTLS_SHA256_C) || !defined(MBEDTLS_SHA384_C)
#error "Security override requires TLS 1.2 ECDHE-ECDSA AES-GCM SHA256/SHA384"
#endif
#if defined(CONFIG_MBEDTLS_DYNAMIC_BUFFER)
#error "Reaudit dynamic TLS buffer destruction before enabling it"
#endif
"""
TLS_POLICY = """ /* mbedTLS retains this pointer: it must outlive every server session. */
static const int security_server_ciphersuites[] = {
MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
0
};
mbedtls_ssl_conf_ciphersuites(&tls->conf, security_server_ciphersuites);
mbedtls_ssl_conf_min_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_2);
mbedtls_ssl_conf_max_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_2);
#if defined(MBEDTLS_SSL_RENEGOTIATION)
mbedtls_ssl_conf_renegotiation(&tls->conf, MBEDTLS_SSL_RENEGOTIATION_DISABLED);
#endif
"""
# Official patches (TLS 1.2 hunk only for EMS):
# https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch
# https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch
# https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch
# DHCP uses equivalent remaining-length checks to avoid forming pointers beyond
# the input object. Keep original upstream notices verbatim, rather than changing
# their copyright year; the central project modification notice is separate.
# Bounded server parser subset of official wolfSSL/wolfssh PRs 892, 881,
# and 880, plus the current-server PR899/902 disposition documented in
# docs/ssh_parser_remaining_review.md. Preserve ordering/password/async edits.
# GetSize already uses safe remaining lengths.
WOLFSSH_PARSER_EDITS = (
Edit("""int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
int result;
word32 strSz;
result = GetUint32(&strSz, buf, len, idx);
""", """int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
int result;
word32 strSz;
if (*sSz == 0)
return WS_BUFFER_E;
result = GetSize(&strSz, buf, len, idx);
"""),
Edit(""" result = GetUint32(&sz, buf, len, idx);
if (result == WS_SUCCESS) {
result = WS_BUFFER_E;
if (*idx < len && sz <= len - *idx) {""", """ result = GetSize(&sz, buf, len, idx);
if (result == WS_SUCCESS) {
result = WS_BUFFER_E;
if (*idx <= len && sz <= len - *idx) {"""),
Edit(""" word32 dataSz;
word32 begin = *idx;
WOLFSSH_UNUSED(ssh);
WOLFSSH_UNUSED(len);
ato32(buf + begin, &dataSz);
begin += LENGTH_SZ + dataSz;
*idx = begin;
return WS_SUCCESS;""", """ WOLFSSH_UNUSED(ssh);
return GetSkip(buf, len, idx);"""),
Edit(""" WOLFSSH_UNUSED(len);
ato32(buf + begin, &nameSz);
begin += LENGTH_SZ;
if (begin + nameSz > len || nameSz >= WOLFSSH_MAX_NAMESZ) {
return WS_BUFFER_E;
}
WMEMCPY(serviceName, buf + begin, nameSz);
begin += nameSz;
serviceName[nameSz] = 0;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);""", """ int ret = GetSize(&nameSz, buf, len, &begin);
/* Preserve 1.4.20's service-name limit; GetString normally truncates. */
if (ret != WS_SUCCESS || nameSz >= sizeof(serviceName))
return WS_BUFFER_E;
begin = *idx;
nameSz = sizeof(serviceName);
ret = GetString(serviceName, &nameSz, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
/* PR902 current-server subset: reject before publishing the transition.
* The owner closes on this error; no best-effort disconnect is queued. */
if (nameSz != sizeof("ssh-userauth") - 1 ||
WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0)
return WS_INVALID_STATE_E;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);"""),
# PR899 fixes the reversed length predicate. The pinned handler additionally
# needs a bounded recipient parser, not merely the later-tree one-line fix.
Edit(""" if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
ret = WS_CHANOPEN_FAILED;""", """ word32 begin, channelId;
if (ssh == NULL || buf == NULL || idx == NULL)
return WS_BAD_ARGUMENT;
begin = *idx;
ret = GetUint32(&channelId, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
if (begin != len)
return WS_BUFFER_E;
if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)
return WS_INVALID_CHANID;
*idx = begin;
ret = WS_CHANOPEN_FAILED;"""),
Edit(""" channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
channel->peerWindowSz);""", """ if (bytesToAdd > (word32)0xFFFFFFFFU - channel->peerWindowSz) {
ret = WS_OVERFLOW_E;
}
else {
channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
channel->peerWindowSz);
}"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Public Key's type does not match public key type");""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Public Key's type does not match public key type");"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);"""),
# Local framing correction: GetSize proves i + sz cannot overflow. Bound
# both mpints to that sub-blob, then reject unconsumed inner/outer bytes.
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&sSz, &s, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz,""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
/* GetSize bounded sz by signatureSz - i: this end cannot wrap. */
sz += i;
ret = GetStringRef(&rSz, &r, pk->signature, sz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&sSz, &s, pk->signature, sz, &i);
}
if (ret == WS_SUCCESS && (i != sz || sz != pk->signatureSz))
ret = WS_BUFFER_E;
if (ret == WS_SUCCESS) {
ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz,"""),
# PR 880's remaining current-feature label checks (Ed25519).
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz
&& WMEMCMP(publicKeyType,
pk->publicKeyType, publicKeyTypeSz) != 0) {""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz
|| WMEMCMP(publicKeyType,
pk->publicKeyType, publicKeyTypeSz) != 0) {"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
/* The signature string must consume the enclosing signature field. */
if (ret == WS_SUCCESS && sz != pk->signatureSz - i)
ret = WS_BUFFER_E;
if (ret == WS_SUCCESS) {
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,"""),
)
# DoChannelRequest's bounded GetString may truncate at 31 bytes; all recognized
# names are shorter. Exact length first prevents short-name reads and aliases;
# memcmp (not strncmp) also rejects embedded NULs. Keep branch bodies unchanged.
WOLFSSH_PARSER_EDITS += tuple(
Edit(f'WSTRNCMP(type, "{name}", typeSz) == 0',
f'typeSz == sizeof("{name}") - 1 &&\n'
f' WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0')
for name in ("env", "shell", "exec", "subsystem", "pty-req", "window-change",
"exit-status", "exit-signal", "auth-agent-req@openssh.com")
)
ENTRIES = (
Entry("dhcpserver", "lwip", "idf",
"components/lwip/apps/dhcpserver/dhcpserver.c",
"953f46189bc64680ea5fa761e75511fadb3aebf698a0d9dff251d77166d78b80", (
Edit("#define DHCP_OPTION_SUBNET_MASK 1",
"#define DHCP_OPTION_PAD 0\n#define DHCP_OPTION_SUBNET_MASK 1"),
Edit(" bool is_dhcp_parse_end = false;\n", ""),
Edit(" switch ((s16_t) *optptr) {", """ if (*optptr == DHCP_OPTION_PAD) {
optptr++;
continue;
}
if (*optptr == DHCP_OPTION_END) {
break;
}
if (end - optptr < 2) {
break;
}
u8_t opt_len = optptr[1];
if (opt_len > end - optptr - 2) {
break;
}
switch ((s16_t) *optptr) {"""),
Edit(" type = *(optptr + 2);", """ if (opt_len >= 1) {
type = optptr[2];
}"""),
Edit(""" if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\\n");
#endif
s.state = DHCPS_STATE_ACK;
} else {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\\n");
#endif
s.state = DHCPS_STATE_NAK;
}""", """ if (opt_len >= 4) {
if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\\n");
#endif
s.state = DHCPS_STATE_ACK;
} else {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\\n");
#endif
s.state = DHCPS_STATE_NAK;
}
}"""),
Edit("""
case DHCP_OPTION_END: {
is_dhcp_parse_end = true;
}
break;
}
if (is_dhcp_parse_end) {
break;
}
optptr += optptr[1] + 2;""", """
}
optptr += opt_len + 2;"""),
)),
Entry("mbedtls_ssl_tls", "mbedtls", "idf",
"components/mbedtls/mbedtls/library/ssl_tls.c",
"0154e70f20b465213d3f97a9c2b75e686d79ee83592f90e1d7e7d995593ca7e1", (
Edit(' MBEDTLS_SSL_DEBUG_RET(1, "calc_verify", ret);\n',
' MBEDTLS_SSL_DEBUG_RET(1, "calc_verify", ret);\n return ret;\n'),
), target="mbedtls"),
Entry("mbedtls_x509_create", "mbedtls", "idf",
"components/mbedtls/mbedtls/library/x509_create.c",
"55edce5b8fcb039a404b84ea830f06d018128b155dfbf0d74d26b17247b546c6", (
Edit(" oid.p = mbedtls_calloc(1, oid.len);\n",
""" oid.p = mbedtls_calloc(1, oid.len);
if (oid.p == NULL) {
return MBEDTLS_ERR_X509_ALLOC_FAILED;
}
"""),
), target="mbedx509"),
Entry("wolfssh_internal", "wolfssl__wolfssh", "project",
"managed_components/wolfssl__wolfssh/src/internal.c",
"81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", WOLFSSH_ORDER_EDITS["src/internal.c"] + WOLFSSH_PARSER_EDITS + (
Edit(""" WS_UserAuthData_Password* pw = NULL;
int ret = WS_SUCCESS;
""", """ WS_UserAuthData_Password* pw = NULL;
word32 passwordStart = 0;
int wipePassword = 0;
int ret = WS_SUCCESS;
"""),
Edit(""" WLOG(WS_LOG_DEBUG, "Entering DoUserAuthRequestPassword()");
if (ssh == NULL || authData == NULL ||
buf == NULL || len == 0 || idx == NULL) {
ret = WS_BAD_ARGUMENT;
}
if (ret == WS_SUCCESS) {
begin = *idx;
""", """ WLOG(WS_LOG_DEBUG, "Entering DoUserAuthRequestPassword()");
if (ssh == NULL || authData == NULL ||
buf == NULL || len == 0 || idx == NULL || ssh->ctx == NULL) {
ret = WS_BAD_ARGUMENT;
}
if (ret == WS_SUCCESS && *idx > len)
ret = WS_BUFFER_E;
if (ret == WS_SUCCESS) {
passwordStart = *idx;
wipePassword = 1;
begin = *idx;
"""),
Edit("ret = GetUint32(&pw->passwordSz, buf, len, &begin);",
"ret = GetSize(&pw->passwordSz, buf, len, &begin);"),
Edit("ret = GetUint32(&pw->newPasswordSz, buf, len, &begin);",
"ret = GetSize(&pw->newPasswordSz, buf, len, &begin);"),
Edit(""" if (ssh->ctx->userAuthCb != NULL) {
WLOG(WS_LOG_DEBUG, "DUARPW: Calling the userauth callback");
""", """ if (ret == WS_SUCCESS && ssh->ctx->userAuthCb != NULL) {
WLOG(WS_LOG_DEBUG, "DUARPW: Calling the userauth callback");
"""),
Edit(""" else {
WLOG(WS_LOG_DEBUG, "DUARPW: No user auth callback");
""", """ else if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "DUARPW: No user auth callback");
"""),
Edit(""" if (authFailure || partialSuccess) {
ret = SendUserAuthFailure(ssh, partialSuccess);
}
else if (ret == WS_SUCCESS) {
ssh->clientState = CLIENT_USERAUTH_DONE;
}
WLOG(WS_LOG_DEBUG, "Leaving DoUserAuthRequestPassword(), ret = %d", ret);
""", """ /* Preserve the username/service/method prefix used by the caller.
* Wipe only the checked packet suffix, never callback pointers or declared
* credential sizes. ForceZero is the library's nonoptimizable primitive.
* Async callbacks need this payload for retry: the project never returns
* pending, but this is not a full secret-lifetime guarantee for async users. */
if (wipePassword && ret != WS_AUTH_PENDING)
ForceZero(buf + passwordStart, len - passwordStart);
if (authFailure || partialSuccess) {
ret = SendUserAuthFailure(ssh, partialSuccess);
}
else if (ret == WS_SUCCESS) {
ssh->clientState = CLIENT_USERAUTH_DONE;
}
WLOG(WS_LOG_DEBUG, "Leaving DoUserAuthRequestPassword(), ret = %d", ret);
"""),
)),
Entry("https_server", "esp_https_server", "idf",
"components/esp_https_server/src/https_server.c",
"a2a5ca0549fbe8d1ddd7f9647a48a31fdd6329b997e1550ab69d6a10efecff0d", (
Edit('const static char *TAG = "esp_https_server";\n',
WIPE + 'const static char *TAG = "esp_https_server";\n'),
Edit(""" if (!transport_ctx) {
esp_https_server_last_error_t last_error = {0};
last_error.last_error = ESP_ERR_NO_MEM;
http_dispatch_event_to_event_loop(HTTPS_SERVER_EVENT_ERROR, &last_error, sizeof(last_error));
return ESP_ERR_NO_MEM;
}
""", """ if (!transport_ctx) {
esp_https_server_last_error_t last_error = {0};
last_error.last_error = ESP_ERR_NO_MEM;
http_dispatch_event_to_event_loop(HTTPS_SERVER_EVENT_ERROR, &last_error, sizeof(last_error));
esp_tls_server_session_delete(tls);
return ESP_ERR_NO_MEM;
}
"""),
Edit(""" if (cfg->serverkey_buf) {
free((void *)cfg->serverkey_buf);
}
""", """ if (cfg->serverkey_buf) {
security_override_wipe((void *)cfg->serverkey_buf, cfg->serverkey_bytes);
free((void *)cfg->serverkey_buf);
}
"""),
Edit(""" ret = httpd_start(&handle, &config->httpd);
if (ret != ESP_OK) {
free(ssl_ctx);
ssl_ctx = NULL;
return ret;
}
""", """ ret = httpd_start(&handle, &config->httpd);
if (ret != ESP_OK) {
if (ssl_ctx != NULL) {
config->httpd.open_fn = ssl_ctx->open_fn;
free_secure_context(ssl_ctx);
config->httpd.global_transport_ctx = NULL;
config->httpd.global_transport_ctx_free_fn = NULL;
}
return ret;
}
"""),
)),
Entry("httpd_parse", "esp_http_server", "idf",
"components/esp_http_server/src/httpd_parse.c",
"db7fbbb322bccb4a21bc1607208a6bb7d1f4c0d7ecc3299de10c9e60edaa943b", (
Edit('static const char *TAG = "httpd_parse";\n',
WIPE + SCRATCH_RESIZE + 'static const char *TAG = "httpd_parse";\n'),
Edit(" size_t at_offset = parser_data->last.at - raux->scratch;\n",
""" /* No parser position exists until a callback sets it. In particular,
* parse_init/init_req_aux start with both pointers NULL. */
bool has_at = parser_data->last.at != NULL && raux->scratch != NULL;
size_t at_offset = has_at ? parser_data->last.at - raux->scratch : 0;
"""),
Edit(" parser_data->last.at = raux->scratch + at_offset;\n",
" parser_data->last.at = has_at ? raux->scratch + at_offset : NULL;\n"),
Edit(""" raux->scratch = (char*) realloc(raux->scratch, offset + buf_len);
if (raux->scratch == NULL) {
""", """ if (!security_override_resize_scratch(raux, offset + buf_len)) {
"""),
Edit(" raux->scratch_cur_size = offset + buf_len;\n", ""),
Edit(""" free(ra->scratch);
ra->scratch = NULL;
""", """ security_override_wipe(ra->scratch, ra->scratch_cur_size);
free(ra->scratch);
ra->scratch = NULL;
"""),
)),
Entry("esp_tls_mbedtls", "esp-tls", "idf",
"components/esp-tls/esp_tls_mbedtls.c",
"edc39052244526cb91c93a16bc765194031060e5560fd432e75542f2f6c8db53", (
Edit('static const char *TAG = "esp-tls-mbedtls";\n',
TLS_GUARDS + 'static const char *TAG = "esp-tls-mbedtls";\n'),
Edit(" mbedtls_ssl_conf_set_user_data_p(&tls->conf, cfg->userdata);\n",
TLS_POLICY + " mbedtls_ssl_conf_set_user_data_p(&tls->conf, cfg->userdata);\n"),
)),
# 5.5.3 introduced blocking header reads, but sizeof promotes negative
# receive errors to unsigned. These fixed header extents are at most 8 bytes.
Entry("httpd_ws", "esp_http_server", "idf",
"components/esp_http_server/src/httpd_ws.c",
"a02194bc8adb1a1707f680390f30ccfab95cd83230cbe4aedcadda2999c9649c",
tuple(Edit(f"HTTPD_RECV_OPT_BLOCKING) < sizeof({name}))",
f"HTTPD_RECV_OPT_BLOCKING) < (int)sizeof({name}))")
for name in ("second_byte", "aux->mask_key", "first_byte")) +
tuple(Edit(f'HTTPD_RECV_OPT_BLOCKING) < sizeof(length_bytes)) {{\n'
f' ESP_LOGW(TAG, LOG_FMT("Failed to receive {size} bytes length"));',
f'HTTPD_RECV_OPT_BLOCKING) < (int)sizeof(length_bytes)) {{\n'
f' ESP_LOGW(TAG, LOG_FMT("Failed to receive {size} bytes length"));')
for size in (2, 8))),
)
ENTRIES += (
Entry("wolfssh_ssh", "wolfssl__wolfssh", "project",
"managed_components/wolfssl__wolfssh/src/ssh.c",
"a4f479ff87eea0980ec1ebdf2c7dd090da473780181b695a56799cb9611f4366",
WOLFSSH_ORDER_EDITS["src/ssh.c"]),
Entry("wolfssh_internal_header", "wolfssl__wolfssh", "project",
"managed_components/wolfssl__wolfssh/wolfssh/internal.h",
"8e417149a68f8a6c0506957adf014b3e6c1727a723536826ce5fb0c9e1f1aba3",
WOLFSSH_ORDER_EDITS["wolfssh/internal.h"], header=True),
)
def apply_edits(text: str, edits: tuple[Edit, ...]) -> str:
for index, edit in enumerate(edits, 1):
count = text.count(edit.old) if edit.old else 0
if count != 1:
raise OverrideError(f"edit {index}: expected exactly one match, got {count}")
text = text.replace(edit.old, edit.new, 1)
return text
def verify_version(idf: Path) -> Path:
version = idf / "components/esp_common/include/esp_idf_version.h"
text = version.read_text(encoding="utf-8")
for part, expected in (("MAJOR", "5"), ("MINOR", "5"), ("PATCH", "3")):
found = re.findall(r"^#define ESP_IDF_VERSION_" + part + r"\s+(\d+)\s*$", text, re.M)
if found != [expected]:
raise OverrideError(f"requires ESP-IDF 5.5.3: {version} ({part}={found})")
return version
def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]:
if not all(re.fullmatch(r"[a-zA-Z0-9_-]+", value) for value in (entry.name, entry.component)):
raise OverrideError("invalid entry name/component")
if entry.target and (entry.component != "mbedtls" or entry.target not in
{"mbedtls", "mbedx509", "mbedcrypto"}):
raise OverrideError("invalid nested target selection")
if entry.header and (entry.name != "wolfssh_internal_header" or
entry.component != "wolfssl__wolfssh" or entry.target or
entry.root != "project" or entry.source !=
"managed_components/wolfssl__wolfssh/wolfssh/internal.h"):
raise OverrideError("unaudited header overlay")
root = roots[entry.root].resolve()
source = (root / entry.source).resolve()
if not source.is_relative_to(root):
raise OverrideError(f"source escapes {entry.root}: {entry.source}")
raw = source.read_bytes()
actual = hashlib.sha256(raw).hexdigest()
if actual != entry.sha256:
raise OverrideError(f"{entry.name}: SHA256 mismatch for {source}: expected {entry.sha256}, got {actual}; reaudit, do not repin blindly")
notice = MODIFICATION_NOTICE
if entry.root == "idf":
notice += ("/* Rebased to audited ESP-IDF 5.5.3 originals on 2026-09-16.\n"
" * All local corrections retained; see docs/idf_candidate_integration.md.\n"
" */\n")
if entry.component == "wolfssl__wolfssh":
notice += ("/* Ordering profile modified 2026-09-16: PR793/819/840/855/921\n"
" * plus project restricted no-EXT_INFO correction. Provenance and\n"
" * limitations: tools/wolfssh_order/README.md and delta.json.\n"
" */\n")
if entry.name == "wolfssh_internal":
notice += ("/* Server parser review modified 2026-09-16: bounded CHANNEL_FAILURE\n"
" * and ssh-userauth service validation; PR899/902 subset, not full PRs.\n"
" * Local follow-up: exact bounded channel-request names.\n"
" * Provenance/limits: docs/ssh_parser_remaining_review.md.\n"
" */\n")
if entry.header:
notice += ("#if defined(_WOLFSSH_INTERNAL_H_) && \\\n"
" (!defined(SAK_WOLFSSH_ORDER_ABI) || SAK_WOLFSSH_ORDER_ABI != 20260916)\n"
'#error "Security override: stale wolfSSH internal.h included before overlay"\n'
"#endif\n#define SAK_WOLFSSH_ORDER_ABI 20260916\n")
return source, (notice + apply_edits(raw.decode("utf-8"), entry.edits)).encode("utf-8")
def write_if_changed(path: Path, data: bytes) -> bool:
if path.exists() and path.read_bytes() == data:
return False
path.parent.mkdir(parents=True, exist_ok=True)
name = None
try:
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as output:
name = output.name
output.write(data)
os.replace(name, path)
finally:
if name and os.path.exists(name):
os.unlink(name)
return True
def cmake_quote(value: str) -> str:
# Semicolons/newlines would turn one source into several CMake list entries.
if any(c in value for c in ";\n\r"):
raise OverrideError("unsupported character in CMake path")
return '"' + value.replace("\\", "/").replace('"', '\\"').replace("$", "\\$") + '"'
def generate(idf: Path, project: Path, binary: Path, entries: tuple[Entry, ...] = ENTRIES) -> Path:
idf, project, binary = idf.resolve(), project.resolve(), binary.resolve()
if binary.is_relative_to(idf) or binary == project or idf.is_relative_to(binary):
raise OverrideError("binary directory must be separate from SDK and project source root")
version = verify_version(idf)
roots = {"idf": idf, "project": project}
names = [entry.name for entry in entries]
if not names or len(set(names)) != len(names):
raise OverrideError("absent or duplicate override entries")
output = binary / "security_overrides"
rendered = []
seen = set()
# Validate the entire plan before writing any generated source or manifest.
for entry in entries:
source, data = render_entry(entry, roots)
if source in seen:
raise OverrideError(f"ambiguous duplicate source: {source}")
seen.add(source)
target = (output / "wolfssh_include" / "wolfssh" / source.name
if entry.header else output / entry.name / source.name)
rendered.append((entry, source, target, data))
lines = ["# Generated by tools/security_overrides.py; do not edit.",
"set(SAK_SECURITY_OVERRIDE_IDS " + " ".join(e.name for e in entries if not e.header) + ")",
"set(SAK_SECURITY_HEADER_IDS " + " ".join(e.name for e in entries if e.header) + ")",
"set(SAK_SECURITY_WOLFSSH_INCLUDE " + cmake_quote(str(output / "wolfssh_include")) + ")",
"set(SAK_SECURITY_VERSION_HEADER " + cmake_quote(str(version)) + ")"]
for entry, source, target, _ in rendered:
for key, value in (("COMPONENT", entry.component), ("TARGET", entry.target), ("ORIGINAL", str(source)),
("GENERATED", str(target)), ("SHA256", entry.sha256)):
lines.append(f"set(SAK_SECURITY_{entry.name}_{key} {cmake_quote(value)})")
manifest = output / "manifest.cmake"
manifest_data = ("\n".join(lines) + "\n").encode("utf-8")
for path in [manifest] + [item[2] for item in rendered]:
resolved = path.resolve()
if not resolved.is_relative_to(binary) or resolved.is_relative_to(idf) or resolved in seen:
raise OverrideError(f"output escapes binary directory or aliases a source: {path}")
for _, _, target, data in rendered:
write_if_changed(target, data)
write_if_changed(manifest, manifest_data)
return manifest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--idf-path", type=Path, required=True)
parser.add_argument("--project-dir", type=Path, required=True)
parser.add_argument("--binary-dir", type=Path, required=True)
args = parser.parse_args()
try:
generate(args.idf_path, args.project_dir, args.binary_dir)
except (OverrideError, OSError, UnicodeError, KeyError) as error:
print(f"security overrides: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())