- Enforce exact service and channel names with bounded failure parsing - Add hash-pinned offline notice assembly and regression coverage - Record advisory dispositions, provenance, integration evidence, and remaining gates
786 lines
31 KiB
Python
786 lines
31 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",
|
|
"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_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",
|
|
"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"),
|
|
)),
|
|
)
|
|
|
|
|
|
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", "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")
|
|
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.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())
|