Add Phase 9C security hardening

Generate exact-hash SDK source overrides without modifying dependencies.
Harden
SSH allocation and algorithm policy, tighten web authentication cleanup,
and add
focused host contract tests and documentation.
This commit is contained in:
2026-09-15 22:12:57 +02:00
parent 751dfb9ddb
commit cdc9c7335a
41 changed files with 3597 additions and 89 deletions
+134
View File
@@ -0,0 +1,134 @@
# Pinned SDK security overrides
The root `CMakeLists.txt` includes `cmake/security_overrides.cmake` **after**
`project()`. No component/vendor file is edited, and no global crypto feature or
client ciphersuite setting is changed.
## Build contract
`tools/security_overrides.py` requires the installed ESP-IDF version header to
identify **5.5.0**, and checks each complete original source against its reviewed
SHA256. Every text substitution must match **exactly once**. The entire input
plan is validated before any output is written. A changed SDK, missing source,
ambiguous edit, duplicate source, or missing/ambiguous component target fails
configuration; there is no unpatched fallback or automatic hash repinning.
Derived **full files**, retaining the original copyright/license notices, live
only at `<CMAKE_BINARY_DIR>/security_overrides/<entry-name>/<basename>`.
The manifest and derived files are atomically replaced only when their bytes
change. Output paths cannot escape the binary tree or alias SDK/source files.
CMake tracks the generator, version header, originals, and derived sources for
reconfiguration. The included CMake file is itself an ordinary CMake input.
Requirements: Python 3.9+ and CMake 3.18+ (directory-scoped source properties).
CMake replaces the exact original entry in the existing component's `SOURCES`;
it does not add a second definition or replace the component target. Target
compile settings remain intact. Source compile flags/options/definitions,
per-configuration definitions, source includes and object dependencies are
copied in the target's owning directory. The original C file's directory is
prepended to that source's include search path, preserving quoted local headers.
Source generator expressions are rejected rather than guessed through.
### Current corrections
- `esp_https_server:src/https_server.c`: delete TLS if post-handshake transport
allocation fails; destroy the complete secure context if HTTPD start fails;
wipe exactly `serverkey_bytes` before releasing the raw private-key copy.
Failed start restores the original open callback and clears stale transport
context/destructor pointers. Failed stop retains live ownership.
- `esp_http_server:src/httpd_parse.c`: allocate/copy/wipe/free scratch resize,
retaining old storage on failure; wipe current scratch on final cleanup.
Initial reads avoid null-pointer subtraction and preserve a null parser
position until a callback sets it; existing positions relocate with scratch.
Existing shrink/grow behavior and bounds remain. Resizing briefly owns old
plus new allocations; no persistent maximum-size buffer, socket, task or
limit increase. Pending/unread bytes are not erased or drained by the patch.
- `esp-tls:esp_tls_mbedtls.c`: **server-local** static-lifetime allowlist of
`TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` and
`TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`, terminated by zero, configured after
server defaults and before setup. Both version limits are TLS 1.2;
renegotiation is disabled (or absent at compile time). Required TLS/ECDHE/
ECDSA/AES/GCM/SHA features are compile-guarded. IDF dynamic buffers are rejected
because their destructor bypasses the audited upstream record-buffer wipe.
`set_client_config()` and the shared handle/setup path are unchanged.
- `wolfssl__wolfssh:src/internal.c`: the fourth override pins wolfSSH 1.4.20's
original source SHA256 to
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`.
`GetSize()` bounds password and new-password fields before authentication;
malformed parsing cannot reach the auth callback. The checked packet suffix
is wiped before failure responses, preserving the caller's prefix. Pending
asynchronous authentication retains the payload for retry; this is **not**
an async secret-lifetime/wipe guarantee. Generated parser/control-flow tests
live in `tests/wolfssh_auth_contract/`.
Clients that cannot negotiate this server profile will no longer connect.
Live interoperability and resource/latency testing remain hardware gates.
These corrections do not claim comprehensive zeroization of every TLS/library
copy, compiler spill, accelerator register, browser buffer or allocator region.
## Parent extension point
Add an `Entry` to `tools/security_overrides.py:ENTRIES` with:
- unique `name`;
- exact IDF `component` name (used by `idf_component_get_property`);
- `root="idf"` for installed IDF sources, or `root="project"` for project/vendor
sources;
- exact relative `source`, full reviewed `sha256`, and a tuple of `Edit(old,new)`
exact-once substitutions.
`render_entry()` validates/patches an entry; `generate()` accepts an explicit
entry tuple as well as the default registry. The manifest maps each entry to
its component, original and derived source. CMake's
`sak_security_replace_source(component original generated)` handles replacement
without backend assumptions. The current registry uses this for three IDF
sources and the project-managed wolfSSH source described above. Update the
corresponding library-specific feature/behavior tests when extending the registry. Multiple
sources in the same real component are supported by the replacement function.
Do not change a source hash merely to make a new SDK configure. Re-audit ownership,
cleanup, feature resolution and patches against that source revision first.
## Validation
```sh
python3 tests/sdk_security_overrides/run.py
python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8
```
Optional `--idf-path` selects an existing installed SDK. Tests need host `cc`,
CMake and Ninja; they install nothing, use temporary directories under `.pio/`, and never edit
the selected SDK. The second command also checks the existing real firmware
Ninja registration: exactly one compilation of each derived source, no original
compilation, and exact generated bytes. It does **not** run a firmware build.
Coverage:
- Generator full-source hashes, version, missing/duplicate/ambiguous inputs,
exact edit counts, validation-before-output, unchanged-byte/mtime idempotence,
unsafe output rejection, and preserved upstream notices.
- Extracted **patched actual functions**, not reimplemented cleanup logic:
HTTPS allocation failure matrix; handshake failure; post-handshake allocation
failure; HTTPD start failure; normal close/stop; failed stop preserving ownership.
The unmodified installed `httpd_stop()` is extracted and separately pinned.
Allocator doubles assert key bytes are zero **before** free, with trailing
canaries to reject over-wiping, and detect leaks/double frees.
- Actual patched scratch helper/read/cleanup functions: grow, shrink, no-change,
null-initial first-read success/failure, nullable parser-position preservation,
resize failure preserving the old pointer, receive errors/timeouts, size bounds, final wipe and
original right-aligned pending-byte behavior. Installed pending/unrecv functions
are separately pinned and executed. This is not a full HTTP parser fuzz test.
- Actual server/client configuration functions with crypto/config doubles:
allowlist order/terminator/static lifetime, defaults failure, PKI failure,
version limits, renegotiation enabled/compiled-out variants, untouched default
and caller-provided client suites. Every required feature is individually
removed in compile-failure tests; dynamic-buffer enablement also fails.
- The actual CMake include under fake IDF target discovery, including missing and
duplicate sources/targets. A separate real host compile tests the project-root
extension, child-directory relative `SOURCES`, quoted and source-specific
includes, source/target flags and per-config source definitions. Changing that
fixture's original file makes the next ordinary build reconfigure and reject
its hash instead of compiling stale derived code.
No tests here perform real TLS handshakes, network/device operations, allocation
failure on the target, or whole-Phase-9 hardware acceptance.
+68
View File
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { void *p; size_t n; bool wipe; } allocation;
static allocation allocations[64];
static unsigned live, calls, fail_at, wiped_frees;
static size_t secret_size;
static bool all_secret;
static void *test_alloc(size_t n, bool clear)
{
if (++calls == fail_at) return NULL;
void *p = clear ? calloc(1, n + 16) : malloc(n + 16);
assert(p);
if (!clear) memset(p, 0xa5, n);
memset((unsigned char *)p + n, 0x7b, 16);
for (unsigned i = 0; i < 64; ++i) if (!allocations[i].p) {
allocations[i] = (allocation){p, n, all_secret || n == secret_size};
++live;
return p;
}
abort();
}
static void test_free(void *p)
{
if (!p) return;
for (unsigned i = 0; i < 64; ++i) if (allocations[i].p == p) {
for (size_t j = 0; j < 16; ++j)
assert(((unsigned char *)p)[allocations[i].n + j] == 0x7b);
if (allocations[i].wipe) {
for (size_t j = 0; j < allocations[i].n; ++j)
assert(((unsigned char *)p)[j] == 0);
++wiped_frees;
}
memset(p, 0xdd, allocations[i].n);
allocations[i].p = NULL;
--live;
free(p);
return;
}
assert(!"double free or unowned allocation");
}
static void mark_secret(void *p)
{
for (unsigned i = 0; i < 64; ++i) if (allocations[i].p == p) {
allocations[i].wipe = true;
return;
}
abort();
}
#define malloc(n) test_alloc((n), false)
#define calloc(n, s) test_alloc((n) * (s), true)
#define free(p) test_free(p)
#define ESP_LOGE(...) ((void)0)
#define ESP_LOGD(...) ((void)0)
#define ESP_LOGI(...) ((void)0)
#define ESP_LOGW(...) ((void)0)
#define ESP_OK 0
#define ESP_FAIL -1
#define ESP_ERR_NO_MEM -2
#define ESP_ERR_INVALID_ARG -3
#define ESP_ERR_INVALID_STATE -4
#define ESP_ERR_NOT_SUPPORTED -5
typedef int esp_err_t;
+159
View File
@@ -0,0 +1,159 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include "alloc.h"
typedef void *httpd_handle_t;
typedef int (*httpd_open_func_t)(httpd_handle_t, int);
typedef void esp_https_server_user_cb(void *);
typedef struct { unsigned char secret[37]; } esp_tls_t;
typedef void *esp_tls_error_handle_t;
typedef struct { int last_error, esp_tls_error_code, esp_tls_flags; } esp_https_server_last_error_t;
typedef struct { int user_cb_state; esp_tls_t *tls; } esp_https_server_user_cb_arg_t;
typedef struct {
const unsigned char *cacert_buf, *servercert_buf, *serverkey_buf;
unsigned cacert_bytes, servercert_bytes, serverkey_bytes;
void *userdata; const char **alpn_protos;
unsigned tls_handshake_timeout_ms; bool use_secure_element;
} esp_tls_cfg_server_t;
typedef struct {
void *global_transport_ctx, *global_user_ctx;
void (*global_transport_ctx_free_fn)(void *), (*global_user_ctx_free_fn)(void *);
httpd_open_func_t open_fn;
int server_port, ctrl_port;
} httpd_config_t;
struct httpd_ssl_config {
httpd_config_t httpd;
bool session_tickets, use_secure_element, use_ecdsa_peripheral;
const unsigned char *cacert_pem, *servercert, *prvtkey_pem;
unsigned cacert_len, servercert_len, prvtkey_len, tls_handshake_timeout_ms;
void *ssl_userdata; const char **alpn_protos;
esp_https_server_user_cb *user_cb;
int transport_mode, port_secure, port_insecure;
};
struct httpd_data {
httpd_config_t config; int msg_fd;
struct { int status; } hd_td;
void *transport;
void (*close_fn)(void *);
};
struct httpd_ctrl_data { int hc_msg; };
#define HTTPD_SSL_TRANSPORT_SECURE 1
#define HTTPD_SSL_USER_CB_SESS_CLOSE 2
#define HTTPD_SSL_USER_CB_SESS_CREATE 3
#define HTTPS_SERVER_EVENT_ERROR 4
#define HTTPS_SERVER_EVENT_ON_CONNECTED 5
#define HTTPS_SERVER_EVENT_DISCONNECTED 6
#define HTTPS_SERVER_EVENT_START 7
#define HTTPS_SERVER_EVENT_STOP 8
#define HTTP_SERVER_EVENT_STOP 9
#define HTTPD_CTRL_SHUTDOWN 10
#define THREAD_STOPPED 11
static bool start_failure, stop_failure, handshake_failure;
static unsigned deletes, creates, closes;
static struct httpd_data *active;
static void http_dispatch_event_to_event_loop(int id, const void *v, size_t n) {}
static void esp_http_server_dispatch_event(int id, const void *v, size_t n) {}
static int esp_tls_cfg_server_session_tickets_init(esp_tls_cfg_server_t *cfg) { return 0; }
static void esp_tls_cfg_server_session_tickets_free(esp_tls_cfg_server_t *cfg) {}
static esp_tls_t *esp_tls_init(void) {
esp_tls_t *tls = calloc(1, sizeof(*tls));
if (tls) { memset(tls, 0xb6, sizeof(*tls)); mark_secret(tls); }
return tls;
}
static int esp_tls_server_session_create(esp_tls_cfg_server_t *cfg, int fd, esp_tls_t *tls) {
return handshake_failure ? -1 : 0;
}
static void esp_tls_server_session_delete(esp_tls_t *tls) {
assert(tls); ++deletes; memset(tls, 0, sizeof(*tls)); free(tls);
}
static int esp_tls_get_error_handle(esp_tls_t *tls, esp_tls_error_handle_t *e) { return -1; }
static int esp_tls_get_and_clear_last_error(esp_tls_error_handle_t e, int *a, int *b) { return 0; }
static void *httpd_get_global_transport_ctx(httpd_handle_t h) { return ((struct httpd_data *)h)->config.global_transport_ctx; }
static void httpd_sess_set_transport_ctx(httpd_handle_t h, int fd, void *ctx, void (*fn)(void *)) {
struct httpd_data *hd = h; assert(!hd->transport); hd->transport = ctx; hd->close_fn = fn;
}
static int httpd_ssl_send(void) { return 0; }
static int httpd_ssl_recv(void) { return 0; }
static int httpd_ssl_pending(void) { return 0; }
static void httpd_sess_set_send_override(httpd_handle_t h, int fd, int (*fn)(void)) {}
static void httpd_sess_set_recv_override(httpd_handle_t h, int fd, int (*fn)(void)) {}
static void httpd_sess_set_pending_override(httpd_handle_t h, int fd, int (*fn)(void)) {}
static int httpd_start(httpd_handle_t *h, httpd_config_t *cfg) {
if (start_failure) return ESP_FAIL;
struct httpd_data *hd = calloc(1, sizeof(*hd));
if (!hd) return ESP_ERR_NO_MEM;
hd->config = *cfg; *h = hd; active = hd; return ESP_OK;
}
static int cs_send_to_ctrl_sock(int fd, int port, void *msg, size_t n) { return stop_failure ? -1 : 0; }
static void httpd_os_thread_sleep(int ms) {
if (active->transport) {
active->close_fn(active->transport);
active->transport = NULL;
}
active->hd_td.status = THREAD_STOPPED;
}
static void httpd_delete(struct httpd_data *hd) { assert(!hd->transport); free(hd); active = NULL; }
static void user_callback(void *arg) {
esp_https_server_user_cb_arg_t *a = arg;
if (a->user_cb_state == HTTPD_SSL_USER_CB_SESS_CREATE) ++creates;
if (a->user_cb_state == HTTPD_SSL_USER_CB_SESS_CLOSE) ++closes;
}
/* SDK_FUNCTIONS */
static struct httpd_ssl_config config(void) {
static unsigned char ca[13], cert[19], key[23];
memset(ca, 1, sizeof(ca)); memset(cert, 2, sizeof(cert)); memset(key, 3, sizeof(key));
return (struct httpd_ssl_config){.transport_mode=HTTPD_SSL_TRANSPORT_SECURE,
.cacert_pem=ca, .cacert_len=sizeof(ca), .servercert=cert, .servercert_len=sizeof(cert),
.prvtkey_pem=key, .prvtkey_len=sizeof(key), .user_cb=user_callback};
}
int main(void) {
secret_size = 23;
for (unsigned fail = 1; fail <= 6; ++fail) {
struct httpd_ssl_config cfg = config(); httpd_handle_t h = NULL;
calls = 0; fail_at = fail;
assert(httpd_ssl_start(&h, &cfg) != ESP_OK);
assert(!h && live == 0);
}
fail_at = 0;
struct httpd_ssl_config cfg = config(); httpd_handle_t h = NULL;
start_failure = true;
unsigned wipes = wiped_frees;
assert(httpd_ssl_start(&h, &cfg) != ESP_OK && live == 0);
assert(wiped_frees == wipes + 1);
assert(!cfg.httpd.global_transport_ctx && !cfg.httpd.global_transport_ctx_free_fn);
assert(!cfg.httpd.open_fn); /* no stale HTTPS wrapper on a retry */
start_failure = false;
assert(httpd_ssl_start(&h, &cfg) == ESP_OK);
assert(httpd_ssl_stop(h) == ESP_OK && live == 0);
h = NULL;
for (unsigned missing = 0; missing < 2; ++missing) {
cfg = config();
if (missing) cfg.prvtkey_pem = NULL; else cfg.servercert = NULL;
assert(httpd_ssl_start(&h, &cfg) != ESP_OK && live == 0);
}
cfg = config();
assert(httpd_ssl_start(&h, &cfg) == ESP_OK);
unsigned baseline = live;
for (unsigned fail = 1; fail <= 2; ++fail) {
calls = 0; fail_at = fail; unsigned before = deletes;
assert(httpd_ssl_open(h, 42) == ESP_ERR_NO_MEM);
assert(live == baseline && !active->transport);
assert(deletes == before + (fail == 2));
}
fail_at = 0; handshake_failure = true;
unsigned before = deletes;
assert(httpd_ssl_open(h, 42) != ESP_OK && live == baseline);
assert(deletes == before + 1);
handshake_failure = false;
assert(httpd_ssl_open(h, 42) == ESP_OK && creates == 1);
void *retained = active->transport; unsigned retained_live = live;
stop_failure = true; before = deletes; wipes = wiped_frees;
assert(httpd_ssl_stop(h) != ESP_OK);
assert(active->transport == retained && live == retained_live);
assert(deletes == before && wiped_frees == wipes && closes == 0);
stop_failure = false;
assert(httpd_ssl_stop(h) == ESP_OK && live == 0 && closes == 1);
assert(deletes == before + 1 && wiped_frees == wipes + 2);
assert(httpd_ssl_stop(NULL) == ESP_ERR_INVALID_ARG);
cfg = config(); cfg.transport_mode = 0; start_failure = true;
assert(httpd_ssl_start(&h, &cfg) != ESP_OK && live == 0);
puts("HTTPS allocation/handshake/start/stop ownership and wipe matrix PASS");
}
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""Read installed SDK sources; compile extracted patched functions with host doubles.
No dependency writes or network. --build-dir additionally verifies a real IDF
build's Ninja source registration; it does not run a firmware build.
"""
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import argparse
from dataclasses import replace
import hashlib
import importlib.util
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location("security_overrides", ROOT / "tools/security_overrides.py")
sdk = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = sdk
SPEC.loader.exec_module(sdk)
TLS_ENTRY = next(e for e in sdk.ENTRIES if e.name == "esp_tls_mbedtls")
def source_path(entry, idf, project=ROOT):
return {"idf": idf, "project": project}[entry.root] / entry.source
AUXILIARY = {
"components/esp_http_server/src/httpd_main.c": "a16ef65069dda13889c67b922f25eb566573983d6c24f01c089a902d5fd26149",
"components/esp_http_server/src/httpd_txrx.c": "7659ad52c32f29b9a08208dc8b22d023edf274047835ed58107d82a47ccce00e",
}
FEATURES = ["MBEDTLS_SSL_PROTO_TLS1_2", "MBEDTLS_SSL_SRV_C",
"MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED", "MBEDTLS_ECDH_C",
"MBEDTLS_ECDSA_C", "MBEDTLS_AES_C", "MBEDTLS_GCM_C",
"MBEDTLS_SHA256_C", "MBEDTLS_SHA384_C"]
def run(command, *, ok=True, cwd=None):
env = dict(os.environ, CCACHE_DISABLE="1", PYTHONDONTWRITEBYTECODE="1",
TMPDIR=str(ROOT / ".pio"))
result = subprocess.run([str(x) for x in command], cwd=cwd, env=env,
capture_output=True, text=True, timeout=60)
if (result.returncode == 0) != ok:
raise AssertionError(f"command: {command}\n{result.stdout}\n{result.stderr}")
return result.stdout + result.stderr
def extract(text, name):
matches = list(re.finditer(r"^[A-Za-z_][\w* \t]*\b" + re.escape(name) + r"\([^;]*?\)\s*\{", text, re.M))
assert len(matches) == 1, (name, len(matches))
start = matches[0].start()
brace = matches[0].end() - 1
depth = 0
tokens = re.finditer(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|[{}]', text[brace:], re.S)
for token in tokens:
if token.group() == "{": depth += 1
elif token.group() == "}":
depth -= 1
if depth == 0: return text[start:brace + token.end()] + "\n"
raise AssertionError(name)
def typedef(text, name):
match = re.search(r"typedef struct " + name + r"(?:_t)? \{.*?\} " + name + r"_t;", text, re.S)
assert match, name
return match.group() + "\n"
def expect_error(function, phrase):
try:
function()
except (sdk.OverrideError, OSError) as error:
assert phrase in str(error), str(error)
else:
raise AssertionError("expected rejection: " + phrase)
def generator_tests(idf, work):
binary = work / "generated"
manifest = sdk.generate(idf, ROOT, binary)
before = {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in binary.rglob("*") if p.is_file()}
assert sdk.generate(idf, ROOT, binary) == manifest
assert before == {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in before}
for entry in sdk.ENTRIES:
original = source_path(entry, idf).read_bytes()
derived = (binary / "security_overrides" / entry.name / Path(entry.source).name).read_bytes()
assert derived.startswith(original[:original.index(b"*/") + 2])
assert derived != original
expect_error(lambda: sdk.apply_edits("x", (sdk.Edit("missing", "z"),)), "got 0")
expect_error(lambda: sdk.apply_edits("xx", (sdk.Edit("x", "z"),)), "got 2")
expect_error(lambda: sdk.generate(idf, ROOT, binary, ()), "absent")
expect_error(lambda: sdk.generate(idf, ROOT, binary, (sdk.ENTRIES[0],) * 2), "duplicate")
expect_error(lambda: sdk.generate(idf, ROOT, binary,
(sdk.ENTRIES[0], replace(sdk.ENTRIES[0], name="alias"))), "ambiguous")
expect_error(lambda: sdk.generate(idf, ROOT, idf / "forbidden"), "separate")
fake = work / "sdk"
version = Path("components/esp_common/include/esp_idf_version.h")
for rel in [version] + [Path(e.source) for e in sdk.ENTRIES if e.root == "idf"]:
target = fake / rel; target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(idf / rel, target)
last = fake / TLS_ENTRY.source
last.write_bytes(last.read_bytes() + b"\n/* changed dependency */\n")
failed = work / "failed"
expect_error(lambda: sdk.generate(fake, ROOT, failed), "SHA256 mismatch")
assert not failed.exists(), "must validate all inputs before output"
# A failed regeneration must not silently update even the first derived file.
expect_error(lambda: sdk.generate(fake, ROOT, binary), "SHA256 mismatch")
assert before == {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in before}
shutil.copyfile(idf / TLS_ENTRY.source, last)
(fake / version).write_text((fake / version).read_text().replace("VERSION_PATCH 0", "VERSION_PATCH 1"))
expect_error(lambda: sdk.generate(fake, ROOT, failed), "5.5.0")
shutil.copyfile(idf / version, fake / version)
last.unlink()
expect_error(lambda: sdk.generate(fake, ROOT, failed), "No such file")
escaped = work / "escaped_output"; escaped.mkdir()
(escaped / "security_overrides").symlink_to(fake, target_is_directory=True)
expect_error(lambda: sdk.generate(idf, ROOT, escaped), "output escapes")
print("Generator exact hashes/version/absent/ambiguous/atomic-plan/idempotence/path safety PASS")
return binary
def extracted_tests(idf, binary, work):
texts = {e.name: (binary / "security_overrides" / e.name / Path(e.source).name).read_text() for e in sdk.ENTRIES}
aux = {}
for rel, expected in AUXILIARY.items():
raw = (idf / rel).read_bytes()
assert hashlib.sha256(raw).hexdigest() == expected, rel
aux[Path(rel).name] = raw.decode()
https = texts["https_server"]
functions = typedef(https, "httpd_ssl_ctx") + typedef(https, "httpd_ssl_transport_ctx")
functions += extract(aux["httpd_main.c"], "httpd_stop")
for name in ("security_override_wipe", "httpd_ssl_close", "httpd_ssl_open",
"free_secure_context", "create_secure_context", "httpd_ssl_start", "httpd_ssl_stop"):
functions += extract(https, name)
source = (HERE / "https.c").read_text().replace("/* SDK_FUNCTIONS */", functions)
compile_run("https", source, work)
scratch = texts["httpd_parse"]
functions = "".join(extract(aux["httpd_txrx.c"], name) for name in ("httpd_recv_pending", "httpd_unrecv"))
functions += "".join(extract(scratch, name) for name in ("security_override_wipe", "security_override_resize_scratch", "read_block", "httpd_req_cleanup"))
compile_run("scratch", (HERE / "scratch.c").read_text().replace("/* SDK_FUNCTIONS */", functions), work)
tls = texts["esp_tls_mbedtls"]
original = (idf / TLS_ENTRY.source).read_text()
assert extract(tls, "set_client_config") == extract(original, "set_client_config")
assert extract(tls, "esp_create_mbedtls_handle") == extract(original, "esp_create_mbedtls_handle")
guards = tls[tls.index("/* The server profile"):tls.index('static const char *TAG = "esp-tls-mbedtls";')]
functions = extract(tls, "set_server_config") + extract(tls, "set_client_config")
source = (HERE / "tls.c").read_text().replace("/* SDK_FUNCTIONS */", functions)
source = source.replace("/* SDK_PKI */", typedef(tls, "esp_tls_pki")).replace("/* TLS_GUARDS */", guards)
defines = ["-D" + f for f in FEATURES]
compile_run("tls", source, work, defines + ["-DMBEDTLS_SSL_RENEGOTIATION", "-DCONFIG_MBEDTLS_SSL_RENEGOTIATION"])
compile_run("tls_no_renegotiation", source, work, defines)
# Compile actual injected guards independently of the behavioral doubles.
guard_file = work / "guards.c"; guard_file.write_text(guards)
for feature in FEATURES:
run(["cc", "-E", "-x", "c", *["-D" + f for f in FEATURES if f != feature], guard_file], ok=False)
run(["cc", "-E", "-x", "c", *defines, "-DCONFIG_MBEDTLS_DYNAMIC_BUFFER", guard_file], ok=False)
print("TLS feature guard matrix (each required feature + dynamic buffer rejection) PASS")
def compile_run(name, source, work, flags=()):
c = work / (name + ".c"); exe = work / name
c.write_text("/* Extracted SDK sections retain their upstream Apache-2.0 license. */\n" + source)
run(["cc", "-std=gnu11", "-O2", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-Wno-unused-function", "-Wno-unused-variable", *flags, "-I", HERE, c, "-o", exe])
print(run([exe]).strip())
def cmake_fixture_tests(idf, work):
# Use real component inputs with mock IDF target discovery. No SDK compilation.
fixture = work / "cmake_fixture"; fixture.mkdir()
lines = ["cmake_minimum_required(VERSION 3.18)", "project(security_fixture C)",
f'set(TEST_IDF "{idf}")',
'function(idf_build_get_property out property)',
' set(${out} "${TEST_IDF}" PARENT_SCOPE)', 'endfunction()',
'function(idf_component_get_property out component property)',
' set(${out} "test_${component}" PARENT_SCOPE)', 'endfunction()']
for e in sdk.ENTRIES:
if e.root == "project":
copied = fixture / e.source
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source_path(e, idf), copied)
lines += [f'add_library(test_{e.component} STATIC "{source_path(e, idf, fixture)}")']
lines += ['if(TEST_MISSING)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} PROPERTY SOURCES missing.c)', 'endif()',
'if(TEST_AMBIGUOUS)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} APPEND PROPERTY SOURCES "{source_path(sdk.ENTRIES[0], idf, fixture)}")', 'endif()',
'if(TEST_TARGET_MISSING)', 'function(idf_component_get_property out component property)',
'set(${out} nonexistent PARENT_SCOPE)', 'endfunction()', 'endif()']
for e in sdk.ENTRIES:
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}" PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")']
lines += [f'include("{ROOT / "cmake/security_overrides.cmake"}")']
for e in sdk.ENTRIES:
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:test_{e.component},SOURCES>")',
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY COMPILE_FLAGS)',
'if(NOT flags STREQUAL "-DSOURCE_FLAG")', 'message(FATAL_ERROR "lost compile flags")', 'endif()',
f'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY INCLUDE_DIRECTORIES)',
f'if(NOT inc MATCHES "{source_path(e, idf, fixture).parent}")', 'message(FATAL_ERROR "lost original quoted include directory")', 'endif()']
(fixture / "CMakeLists.txt").write_text("\n".join(lines) + "\n")
build = work / "cmake_good"
run(["cmake", "-G", "Ninja", "-S", fixture, "-B", build])
for e in sdk.ENTRIES:
source = (build / (e.name + ".sources")).read_text()
assert source == str(build / "security_overrides" / e.name / Path(e.source).name)
ninja = (build / "build.ninja").read_text()
for path in [ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]:
assert str(path) in next(line for line in ninja.splitlines() if ": RERUN_CMAKE" in line), path
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"), ("TEST_TARGET_MISSING", "missing component target")):
output = run(["cmake", "-G", "Ninja", "-S", fixture, "-B", work / flag, "-D" + flag + "=ON"], ok=False)
assert phrase in output, output
print("CMake actual include: exact target replacement/properties/reconfigure/fail-closed matrix PASS")
def extension_fixture_tests(idf, work):
# Prove the extension API, relative SOURCES in a child directory, real quoted
# includes, target/source flags, and automatic fail-closed reconfiguration.
fixture = work / "extension"
for directory in ("cmake", "tools", "component/src", "component/include"):
(fixture / directory).mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / "cmake/security_overrides.cmake", fixture / "cmake/security_overrides.cmake")
c = fixture / "component/src/example.c"
c.write_text('#include "local.h"\n#include "extra.h"\n'
'#if !defined(SOURCE_FLAG) || !defined(SOURCE_DEFINE) || !defined(SOURCE_OPTION) || !defined(TARGET_DEFINE)\n'
'#error "compile properties were lost"\n#endif\n'
'int example(void) { return LOCAL + EXTRA + 1; }\n')
original = c.read_bytes(); digest = hashlib.sha256(original).hexdigest()
(c.parent / "local.h").write_text("#define LOCAL 10\n")
(fixture / "component/include/extra.h").write_text("#define EXTRA 20\n")
(fixture / "component/CMakeLists.txt").write_text('add_library(test_extension STATIC src/example.c)\n'
'target_compile_definitions(test_extension PRIVATE TARGET_DEFINE)\n'
'set_source_files_properties(src/example.c PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" '
'COMPILE_OPTIONS "-DSOURCE_OPTION" COMPILE_DEFINITIONS "SOURCE_DEFINE" '
'COMPILE_DEFINITIONS_DEBUG "CONFIG_DEFINE" INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include")\n')
(fixture / "main.c").write_text('int example(void); int main(void) { return example() != 32; }\n')
wrapper = ('import sys\nfrom pathlib import Path\nsys.dont_write_bytecode = True\n'
f'sys.path.insert(0, {str(ROOT / "tools")!r})\nimport security_overrides as sdk\n'
'import argparse\np=argparse.ArgumentParser()\n'
'[p.add_argument(a, type=Path, required=True) for a in ("--idf-path", "--project-dir", "--binary-dir")]\n'
'a=p.parse_args()\n'
f'e=sdk.Entry("extension", "extension", "project", "component/src/example.c", {digest!r}, '
'(sdk.Edit("LOCAL + EXTRA + 1", "LOCAL + EXTRA + 2"),))\n'
'sdk.generate(a.idf_path, a.project_dir, a.binary_dir, (e,))\n')
(fixture / "tools/security_overrides.py").write_text(wrapper)
(fixture / "CMakeLists.txt").write_text('cmake_minimum_required(VERSION 3.18)\nproject(extension C)\n'
f'set(TEST_IDF "{idf}")\n'
'function(idf_build_get_property out property)\nset(${out} "${TEST_IDF}" PARENT_SCOPE)\nendfunction()\n'
'function(idf_component_get_property out component property)\nset(${out} "test_${component}" PARENT_SCOPE)\nendfunction()\n'
'add_subdirectory(component)\ninclude(cmake/security_overrides.cmake)\n'
'get_property(config_def SOURCE "${SAK_SECURITY_extension_GENERATED}" DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/component" PROPERTY COMPILE_DEFINITIONS_DEBUG)\n'
'if(NOT config_def STREQUAL "CONFIG_DEFINE")\nmessage(FATAL_ERROR "lost per-config source definitions")\nendif()\n'
'add_executable(check main.c)\ntarget_link_libraries(check PRIVATE test_extension)\n')
build = work / "extension_build"
run(["cmake", "-G", "Ninja", "-S", fixture, "-B", build])
run(["cmake", "--build", build]); run([build / "check"])
assert c.read_bytes() == original
generated = build / "security_overrides/extension/example.c"
stamp = generated.stat().st_mtime_ns
run(["cmake", "--build", build]); assert generated.stat().st_mtime_ns == stamp
c.write_bytes(original + b"\n/* upstream changed */\n")
output = run(["cmake", "--build", build], ok=False)
assert "SHA256 mismatch" in output, output
assert generated.stat().st_mtime_ns == stamp
print("Extension mapping + child relative source/includes/flags real compile + automatic mismatch rejection PASS")
def build_registration(build, idf):
ninja = (build / "build.ninja").read_text()
compile_lines = [line for line in ninja.splitlines() if ": C_COMPILER" in line]
for e in sdk.ENTRIES:
generated = build / "security_overrides" / e.name / Path(e.source).name
matches = [line for line in compile_lines if str(generated) in line]
assert len(matches) == 1, (e.name, matches)
assert not any(str(source_path(e, idf)) in line for line in compile_lines), e.name
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": ROOT})[1]
print("Real IDF Ninja registration: each generated source once, originals absent, bytes verified PASS")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--idf-path", type=Path, default=Path.home() / ".platformio/packages/framework-espidf")
parser.add_argument("--build-dir", type=Path)
args = parser.parse_args()
idf = args.idf_path.resolve()
sdk.verify_version(idf)
(ROOT / ".pio").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix="sdk-security-", dir=ROOT / ".pio") as tmp:
work = Path(tmp)
binary = generator_tests(idf, work)
extracted_tests(idf, binary, work)
cmake_fixture_tests(idf, work)
extension_fixture_tests(idf, work)
if args.build_dir: build_registration(args.build_dir.resolve(), idf)
print("SDK security overrides: all requested host checks PASS (not live TLS/hardware)")
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <sys/types.h>
#include "alloc.h"
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define HTTPD_SOCK_ERR_TIMEOUT -10
#define HTTPD_SOCK_ERR_FAIL -11
#define HTTPD_408_REQ_TIMEOUT 408
struct sock_db {
char pending_data[128]; size_t pending_len;
void *ctx; void (*free_ctx)(void *); bool ignore_sess_ctx_changes;
};
struct httpd_req_aux {
struct sock_db *sd;
char *scratch; size_t scratch_cur_size, scratch_size_limit, remaining_len;
};
typedef struct httpd_req {
struct httpd_req_aux *aux; void *sess_ctx, *handle, *user_ctx;
void (*free_ctx)(void *); bool ignore_sess_ctx_changes;
} httpd_req_t;
typedef struct { void *data; } http_parser;
typedef struct { struct { char *at; } last; } parser_data_t;
static int receive_result = 1, receive_calls;
static int httpd_req_handle_err(httpd_req_t *r, int err) { return ESP_FAIL; }
static void httpd_sess_free_ctx(void **ctx, void (*fn)(void *)) { assert(!*ctx); }
static int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t n, bool halt_after_pending);
/* SDK_FUNCTIONS */
static int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t n, bool halt_after_pending) {
++receive_calls;
assert(halt_after_pending);
if (r->aux->sd->pending_len) return (int)httpd_recv_pending(r, buf, n);
if (receive_result <= 0) return receive_result;
memset(buf, 'x', n); return (int)n;
}
static void cleanup(httpd_req_t *r, struct httpd_req_aux *ra, struct sock_db *sd) {
r->aux = ra; ra->sd = sd;
httpd_req_cleanup(r);
assert(!ra->scratch && !ra->scratch_cur_size && !r->aux && !live);
}
int main(void) {
all_secret = true;
struct sock_db sd = {0};
struct httpd_req_aux ra = {.sd=&sd, .scratch_size_limit=64, .remaining_len=37};
httpd_req_t r = {.aux=&ra};
parser_data_t data = {0}; http_parser parser = {.data=&data};
/* Equivalent pointer/size initialization to parse_init/init_req_aux:
* the first read must allocate its own scratch, with no parser position. */
assert(!data.last.at && !ra.scratch && !ra.scratch_cur_size);
unsigned initial_wipes = wiped_frees;
int initial_reads = receive_calls;
fail_at = calls + 1;
assert(read_block(&r, &parser, 0, 8) == 0);
assert(!data.last.at && !ra.scratch && !ra.scratch_cur_size && !live);
assert(receive_calls == initial_reads && wiped_frees == initial_wipes);
cleanup(&r, &ra, &sd);
assert(wiped_frees == initial_wipes);
fail_at = 0;
r.aux = &ra; ra.sd = &sd; ra.scratch_size_limit = 64;
assert(read_block(&r, &parser, 0, 8) == 8);
assert(ra.scratch && ra.scratch_cur_size == 8 && !data.last.at);
assert(receive_calls == initial_reads + 1 && wiped_frees == initial_wipes);
assert(!memcmp(ra.scratch, "xxxxxxxx", 8));
/* A fragmented request can need another read before the URL callback. */
char *initial = ra.scratch;
fail_at = calls + 1;
assert(read_block(&r, &parser, 8, 8) == 0);
assert(ra.scratch == initial && ra.scratch_cur_size == 8 && !data.last.at);
assert(receive_calls == initial_reads + 1 && wiped_frees == initial_wipes);
fail_at = 0;
assert(read_block(&r, &parser, 8, 8) == 8);
assert(ra.scratch != initial && ra.scratch_cur_size == 16 && !data.last.at);
assert(!memcmp(ra.scratch, "xxxxxxxxxxxxxxxx", 16));
assert(wiped_frees == initial_wipes + 1);
cleanup(&r, &ra, &sd);
assert(wiped_frees == initial_wipes + 2);
r.aux = &ra; ra.sd = &sd; ra.scratch_size_limit = 64;
assert(read_block(&r, &parser, 0, 16) == 16);
assert(!data.last.at);
memcpy(ra.scratch, "Cookie: secret!!", 16);
data.last.at = ra.scratch + 7;
char *old = ra.scratch; unsigned before = wiped_frees;
assert(read_block(&r, &parser, 16, 8) == 8);
assert(ra.scratch != old && ra.scratch_cur_size == 24);
assert(!memcmp(ra.scratch, "Cookie: secret!!", 16));
assert(data.last.at == ra.scratch + 7 && wiped_frees == before + 1);
old = ra.scratch; unsigned saved_calls = calls;
assert(security_override_resize_scratch(&ra, 24) && ra.scratch == old && calls == saved_calls);
fail_at = calls + 1;
int reads = receive_calls;
assert(read_block(&r, &parser, 24, 8) == 0);
assert(ra.scratch == old && ra.scratch_cur_size == 24 && receive_calls == reads);
assert(data.last.at == old + 7 && !memcmp(old, "Cookie: secret!!", 16));
assert(ra.remaining_len == 37);
fail_at = 0;
assert(httpd_unrecv(&r, "NEXT-REQUEST", 12) == 12);
assert(read_block(&r, &parser, 4, 4) == 4); /* actual shrink + pending RX */
assert(ra.scratch_cur_size == 8 && !memcmp(ra.scratch, "CookNEXT", 8));
assert(sd.pending_len == 8 && !memcmp(sd.pending_data + 120, "-REQUEST", 8));
assert(ra.remaining_len == 37);
char pending[128]; memcpy(pending, sd.pending_data, sizeof(pending));
cleanup(&r, &ra, &sd);
assert(sd.pending_len == 8 && !memcmp(pending, sd.pending_data, sizeof(pending)));
r.aux = &ra; ra.sd = &sd;
char out[12] = {0}; assert(httpd_recv_pending(&r, out, 8) == 8 && !memcmp(out, "-REQUEST", 8));
ra.scratch_size_limit = 64;
fail_at = calls + 1;
assert(!security_override_resize_scratch(&ra, 8) && !ra.scratch);
cleanup(&r, &ra, &sd); fail_at = 0;
for (int result = 0; result >= -2; --result) {
r.aux = &ra; ra.sd = &sd; ra.scratch_size_limit = 64;
assert(security_override_resize_scratch(&ra, 8)); data.last.at = ra.scratch;
receive_result = result == -2 ? HTTPD_SOCK_ERR_TIMEOUT : result;
assert(read_block(&r, &parser, 0, 8) == HTTPD_SOCK_ERR_FAIL);
int before_reads = receive_calls;
assert(read_block(&r, &parser, 64, 1) == 0 && before_reads == receive_calls);
cleanup(&r, &ra, &sd);
}
puts("HTTPD null-initial read/grow/shrink/failure/final wipe/bounds/pending-unread matrix PASS");
}
+105
View File
@@ -0,0 +1,105 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include "alloc.h"
#define MBEDTLS_SSL_IS_SERVER 1
#define MBEDTLS_SSL_IS_CLIENT 0
#define MBEDTLS_SSL_TRANSPORT_STREAM 0
#define MBEDTLS_SSL_PRESET_DEFAULT 0
#define MBEDTLS_SSL_VERIFY_NONE 0
#define MBEDTLS_SSL_VERIFY_REQUIRED 2
#define MBEDTLS_SSL_VERSION_TLS1_2 0x303
#define MBEDTLS_SSL_RENEGOTIATION_DISABLED 0
#define MBEDTLS_SSL_RENEGOTIATION_ENABLED 1
#define MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 0xc02b
#define MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 0xc02c
#define ESP_ERR_MBEDTLS_SSL_CONFIG_DEFAULTS_FAILED -11
#define ESP_ERR_MBEDTLS_SSL_SET_HOSTNAME_FAILED -12
#define ESP_ERR_MBEDTLS_SSL_SETUP_FAILED -13
#define ESP_INT_EVENT_TRACKER_CAPTURE(...) ((void)0)
/* TLS_GUARDS */
typedef int mbedtls_x509_crt;
typedef int mbedtls_pk_context;
typedef struct {
const int *suites;
int endpoint, min, max, reneg, policy_calls;
void *userdata;
} mbedtls_ssl_config;
typedef struct {
mbedtls_ssl_config conf;
int ssl, servercert, serverkey, clientcert, clientkey, error_handle;
void *cacert_ptr;
} esp_tls_t;
typedef struct {
void *userdata; const char **alpn_protos;
bool use_secure_element, use_ecdsa_peripheral;
const unsigned char *cacert_buf, *servercert_buf, *serverkey_buf, *serverkey_password;
unsigned cacert_bytes, servercert_bytes, serverkey_bytes, serverkey_password_len;
} esp_tls_cfg_server_t;
typedef struct {
bool skip_common_name, use_global_ca_store, use_secure_element, use_ecdsa_peripheral;
const char *common_name; const char **alpn_protos;
void *crt_bundle_attach, *ds_data;
const unsigned char *cacert_buf, *clientcert_buf, *clientkey_buf, *clientkey_password;
const unsigned char *clientcert_pem_buf, *clientkey_pem_buf;
unsigned cacert_bytes, clientcert_bytes, clientkey_bytes, clientkey_password_len;
const int *ciphersuites_list;
} esp_tls_cfg_t;
/* SDK_PKI */
static int defaults_fail, pki_fail;
static const int default_suites[] = {123, 456, 0};
static int mbedtls_ssl_config_defaults(mbedtls_ssl_config *c, int endpoint, int transport, int preset) {
if (defaults_fail) return -1;
*c = (mbedtls_ssl_config){.suites=default_suites, .endpoint=endpoint,
.min=11, .max=22, .reneg=1};
return 0;
}
static void mbedtls_ssl_conf_ciphersuites(mbedtls_ssl_config *c, const int *list) { c->suites=list; ++c->policy_calls; }
static void mbedtls_ssl_conf_min_tls_version(mbedtls_ssl_config *c, int v) { c->min=v; }
static void mbedtls_ssl_conf_max_tls_version(mbedtls_ssl_config *c, int v) { c->max=v; }
static void mbedtls_ssl_conf_renegotiation(mbedtls_ssl_config *c, int v) { c->reneg=v; }
static void mbedtls_ssl_conf_set_user_data_p(mbedtls_ssl_config *c, void *p) { c->userdata=p; }
static void mbedtls_ssl_conf_authmode(mbedtls_ssl_config *c, int mode) {}
static void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config *c, void *p, void *q) {}
static int mbedtls_ssl_set_hostname(void *ssl, const char *host) { return 0; }
static void mbedtls_print_error_msg(int e) {}
static int set_ca_cert(esp_tls_t *tls, const unsigned char *cert, size_t n) { return 0; }
static int set_global_ca_store(esp_tls_t *tls) { return 0; }
static void check_policy(mbedtls_ssl_config *c) {
assert(c->endpoint == MBEDTLS_SSL_IS_SERVER && c->policy_calls == 1);
assert(c->suites[0] == 0xc02b && c->suites[1] == 0xc02c && c->suites[2] == 0);
assert(c->min == 0x303 && c->max == 0x303);
#ifdef MBEDTLS_SSL_RENEGOTIATION
assert(c->reneg == 0);
#endif
}
static int set_pki_context(esp_tls_t *tls, esp_tls_pki_t *pki) {
if (tls->conf.endpoint == MBEDTLS_SSL_IS_SERVER) check_policy(&tls->conf);
return pki_fail ? -1 : 0;
}
/* SDK_FUNCTIONS */
int main(void) {
static const unsigned char cert[] = {1}, key[] = {2};
esp_tls_cfg_server_t cfg = {.servercert_buf=cert, .serverkey_buf=key, .userdata=&cfg};
esp_tls_t server = {0}, second = {0}, client = {0};
defaults_fail = 1;
assert(set_server_config(&cfg, &server) == ESP_ERR_MBEDTLS_SSL_CONFIG_DEFAULTS_FAILED);
assert(!server.conf.policy_calls && !server.conf.suites);
defaults_fail = 0;
assert(set_server_config(&cfg, &server) == 0); check_policy(&server.conf);
assert(server.conf.userdata == &cfg);
assert(set_server_config(&cfg, &second) == 0); check_policy(&second.conf);
assert(server.conf.suites == second.conf.suites); /* retained static lifetime */
esp_tls_cfg_t ccfg = {.skip_common_name=true, .use_global_ca_store=true};
assert(set_client_config("host", 4, &ccfg, &client) == 0);
assert(client.conf.suites == default_suites && !client.conf.policy_calls);
assert(client.conf.min == 11 && client.conf.max == 22 && client.conf.reneg == 1);
static const int custom[] = {999, 0}; ccfg.ciphersuites_list = custom;
assert(set_client_config("host", 4, &ccfg, &client) == 0);
assert(client.conf.suites == custom && client.conf.policy_calls == 1);
check_policy(&server.conf);
pki_fail = 1;
assert(set_server_config(&cfg, &second) != 0); check_policy(&second.conf);
pki_fail = 0; cfg.serverkey_buf = NULL;
assert(set_server_config(&cfg, &second) == ESP_ERR_INVALID_STATE);
assert(!live);
puts("TLS server-only allowlist/version/renegotiation/config-failure/client isolation PASS");
}