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:
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user