Files
ESP32_Serial_Swiss_Army_Knife/tools/security_overrides.py
T
Commander1024 4d3bb490c9 Harden wolfSSL and wolfSSH validation
Enable validated ECC imports and X25519 all-zero rejection through
PUBLIC build policy. Tighten wolfSSH parser bounds, overflow handling,
and signature framing with guard-page and crypto vector contracts.
2026-09-15 23:54:39 +02:00

699 lines
26 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 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.
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.
*/
"""
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 (reviewed alongside PR 899). Keep the 1.4.20 state machine and
# password/async edits below. 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;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);"""),
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,"""),
)
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",
"b726c0c55bc5f32255f129d55f9f2fface85ce83de90a2d16c9017b93b738bff", (
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",
"fd399239aee30384786a19b47bfe5dd22b979d5d89bb38f29f0c82a3d81daaf7", (
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_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",
"6481942b62e51125e2a43441fa0900cbda74bd2ea05c82f0c29eb4933c31946e", (
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",
"6bba77064aaa68a06f8d4c01432064a1b050c91ed22741c547785b0d8a6c07d8", (
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",
"09210c5a601647ca5775d127a2951bab2f3e509192b53487bbea8a93d8731b78", (
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"),
)),
)
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", "0")):
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.0: {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")
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")
return source, (MODIFICATION_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 / 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(names) + ")",
"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())