#!/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())