Apply Phase 9D security mitigations
- Add fail-closed wolfSSL small-math policy and vectors - Backport DHCP, EMS, and X.509 allocation fixes - Extend source override validation and operational documentation
This commit is contained in:
@@ -15,6 +15,9 @@ 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>`.
|
||||
Every generated source is centrally prefixed with an explicit
|
||||
`Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15` notice.
|
||||
This is separate from, and does not rewrite or replace, upstream notices.
|
||||
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
|
||||
@@ -51,7 +54,7 @@ Source generator expressions are rejected rather than guessed through.
|
||||
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
|
||||
- `wolfssl__wolfssh:src/internal.c`: pins wolfSSH 1.4.20's
|
||||
original source SHA256 to
|
||||
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`.
|
||||
`GetSize()` bounds password and new-password fields before authentication;
|
||||
@@ -61,6 +64,26 @@ Source generator expressions are rejected rather than guessed through.
|
||||
an async secret-lifetime/wipe guarantee. Generated parser/control-flow tests
|
||||
live in `tests/wolfssh_auth_contract/`.
|
||||
|
||||
- `lwip:apps/dhcpserver/dhcpserver.c`: PAD/END handling, length-byte and
|
||||
declared-payload bounds, minimum message-type/requested-IP lengths, validated
|
||||
advancement. Backport of official
|
||||
[d51b1076092487e533eadf8b48c9c8579d3a6712](https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch).
|
||||
The only behavioral-code deviation is using equivalent remaining-length
|
||||
comparisons rather than constructing pointers beyond the input object.
|
||||
Original copyright years stay unchanged under the separate modification notice.
|
||||
- Nested `mbedtls:ssl_tls.c`: propagate `calc_verify()` failure immediately,
|
||||
before PRF/master-secret derivation. Exact TLS 1.2 hunk of
|
||||
[f595df4569c1a1650ad9d077e2f2e819e9f1dddb](https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch).
|
||||
EMS remains enabled. This does **not** backport that commit's TLS 1.3 hunk.
|
||||
- Nested `mbedx509:x509_create.c`: reject known-attribute OID allocation failure
|
||||
before `memcpy()`, returning `MBEDTLS_ERR_X509_ALLOC_FAILED`. Exact guard from
|
||||
[bfaf4a47fd33da860796feaba6235847acb71127](https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch).
|
||||
|
||||
These three patches were fetched from the official repositories and compared
|
||||
with the installed pinned sources on 2026-09-15. No dependency versions or
|
||||
existing original-source hashes changed. The optional WS subprotocol backport
|
||||
and separate ASN.1 repeated-OID/empty-value correction are **not** implemented.
|
||||
|
||||
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
|
||||
@@ -75,14 +98,18 @@ Add an `Entry` to `tools/security_overrides.py:ENTRIES` with:
|
||||
- `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.
|
||||
exact-once substitutions;
|
||||
- optional `target` for the explicitly supported `mbedtls`, `mbedx509` or
|
||||
`mbedcrypto` nested library under component `mbedtls`. Empty selects the
|
||||
component's `COMPONENT_LIB` as before. CMake validates the nested target exists,
|
||||
is not imported/an alias, and belongs to the installed mbedTLS library directory.
|
||||
|
||||
`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
|
||||
its component, optional nested target, original and derived source. CMake's
|
||||
`sak_security_replace_source(component original generated nested_target)` handles
|
||||
replacement on the actual source owner, not the IDF mbedTLS wrapper. The current
|
||||
registry has six IDF sources and one project-managed wolfSSH source. 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.
|
||||
|
||||
@@ -98,7 +125,8 @@ python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devk
|
||||
|
||||
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
|
||||
the selected SDK. Run the second command **after the parent runs `pio run`**;
|
||||
old generated copies lack the new entries/notice and must not pass. It checks 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.
|
||||
|
||||
@@ -106,7 +134,8 @@ 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.
|
||||
unsafe output rejection, centrally applied dated modification notice exactly
|
||||
once on every source, and preserved upstream opening 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.
|
||||
@@ -123,8 +152,25 @@ Coverage:
|
||||
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.
|
||||
- Extracted DHCP parser: 196,623 guard-page cases covering empty/lone-code,
|
||||
every option code/length with truncated and complete payloads, PAD, END, short
|
||||
type/IP, DISCOVER/REQUEST/DECLINE/RELEASE, renew, matching/mismatching IP.
|
||||
Compare the remaining-length variant with upstream pointer-form checks on
|
||||
safely padded backing storage; post-loop state behavior is retained.
|
||||
- Extracted TLS master calculation: SHA-256/SHA-384-sized transcript doubles,
|
||||
untouched output and no PRF on hash failure (including unchanged length 64),
|
||||
successful EMS, PRF failure, non-EMS and resumed-session behavior. This does not
|
||||
execute actual hashes, the full handshake driver, or its failure destructor.
|
||||
- Extracted X.509 parser and its actual attribute table/helpers, compiled against
|
||||
installed mbedTLS headers: fault each allocation across separate subject/issuer
|
||||
lists, partial-list cleanup, second-attribute OID failure, and successful retry.
|
||||
Named-data storage/free are allocation-counted doubles; certificate signing,
|
||||
writer destruction, persistence/publication and old-identity retention are not
|
||||
exercised here. The three new C harnesses use UBSan trap instrumentation;
|
||||
this does not require the host UBSan runtime.
|
||||
- The actual CMake include under fake IDF target discovery, including missing and
|
||||
duplicate sources/targets. A separate real host compile tests the project-root
|
||||
duplicate sources/targets, nested target owner validation, and exact Ninja
|
||||
compilation registration on both `mbedtls` and `mbedx509` (not their wrapper). 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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
typedef uint8_t u8_t;
|
||||
typedef uint16_t u16_t;
|
||||
typedef int16_t s16_t;
|
||||
typedef struct { uint32_t addr; } ip4_addr_t;
|
||||
typedef struct { uint32_t client_address; bool renew; } dhcps_t;
|
||||
struct dhcps_state { u8_t state; };
|
||||
enum { DHCPS_STATE_IDLE, DHCPS_STATE_ACK, DHCPS_STATE_NAK, DHCPS_STATE_OFFER,
|
||||
DHCPS_STATE_DECLINE, DHCPS_STATE_RELEASE };
|
||||
/* SDK_DEFINES */
|
||||
/* SDK_FUNCTIONS */
|
||||
static unsigned cases;
|
||||
static u8_t check(dhcps_t *ctx, const u8_t *data, size_t len)
|
||||
{
|
||||
size_t page = (size_t)sysconf(_SC_PAGESIZE);
|
||||
u8_t *map = mmap(NULL, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
assert(map != MAP_FAILED && len <= page);
|
||||
assert(mprotect(map + page, page, PROT_NONE) == 0);
|
||||
u8_t *input = map + page - len;
|
||||
memcpy(input, data, len);
|
||||
u8_t result = parse_options(ctx, input, (s16_t)len);
|
||||
/* Upstream's pointer expressions stay within this larger backing object. */
|
||||
u8_t padded[4096 + 258] = {0};
|
||||
memcpy(padded, data, len);
|
||||
assert(result == upstream_parse_options(ctx, padded, (s16_t)len));
|
||||
assert(memcmp(input, data, len) == 0);
|
||||
assert(munmap(map, page * 2) == 0);
|
||||
cases++;
|
||||
return result;
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
dhcps_t ctx = {.client_address = 0x04030201};
|
||||
u8_t data[300] = {0};
|
||||
assert(check(&ctx, data, 0) == DHCPS_STATE_IDLE);
|
||||
for (unsigned code = 0; code < 256; code++) {
|
||||
data[0] = code;
|
||||
check(&ctx, data, 1);
|
||||
for (unsigned length = 0; length < 256; length++) {
|
||||
data[1] = length;
|
||||
check(&ctx, data, 2);
|
||||
check(&ctx, data, 2 + length);
|
||||
if (length) check(&ctx, data, 1 + length);
|
||||
}
|
||||
}
|
||||
memset(data, 0, sizeof(data));
|
||||
assert(check(&ctx, data, sizeof(data)) == DHCPS_STATE_IDLE);
|
||||
u8_t discover[] = {0, 0, 53, 1, 1, 255};
|
||||
assert(check(&ctx, discover, sizeof(discover)) == DHCPS_STATE_OFFER);
|
||||
u8_t request[] = {53, 1, 3, 50, 4, 0, 0, 0, 0, 255};
|
||||
memcpy(request + 5, &ctx.client_address, 4);
|
||||
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_ACK);
|
||||
request[5] ^= 1;
|
||||
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_NAK);
|
||||
assert(check(&ctx, request, 3) == DHCPS_STATE_NAK);
|
||||
ctx.renew = true;
|
||||
assert(check(&ctx, request, 3) == DHCPS_STATE_ACK);
|
||||
request[2] = 7;
|
||||
assert(check(&ctx, request, 3) == DHCPS_STATE_RELEASE);
|
||||
request[2] = 4;
|
||||
assert(check(&ctx, request, 3) == DHCPS_STATE_DECLINE);
|
||||
u8_t short_type[] = {53, 0, 255};
|
||||
assert(check(&ctx, short_type, sizeof(short_type)) == DHCPS_STATE_IDLE);
|
||||
for (unsigned len = 0; len < 4; len++) {
|
||||
u8_t short_ip[] = {50, len, 1, 2, 3};
|
||||
assert(check(&ctx, short_ip, len + 2) == DHCPS_STATE_IDLE);
|
||||
}
|
||||
u8_t end[] = {255, 53, 1, 1};
|
||||
assert(check(&ctx, end, sizeof(end)) == DHCPS_STATE_IDLE);
|
||||
printf("DHCP extracted parser: %u guard-page / upstream-equivalence cases PASS\n", cases);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
|
||||
#define MBEDTLS_SSL_EXTENDED_MS_ENABLED 1
|
||||
#define MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED -1
|
||||
#define MBEDTLS_SSL_DEBUG_MSG(...) ((void)0)
|
||||
#define MBEDTLS_SSL_DEBUG_RET(...) ((void)0)
|
||||
#define MBEDTLS_SSL_DEBUG_BUF(...) ((void)0)
|
||||
typedef struct { int unused; } mbedtls_ssl_context;
|
||||
typedef struct {
|
||||
int resume, extended_ms;
|
||||
unsigned char randbytes[64], premaster[128];
|
||||
size_t pmslen;
|
||||
int (*calc_verify)(const mbedtls_ssl_context *, unsigned char *, size_t *);
|
||||
int (*tls_prf)(const unsigned char *, size_t, const char *, const unsigned char *, size_t, unsigned char *, size_t);
|
||||
} mbedtls_ssl_handshake_params;
|
||||
static int hash_error, prf_error, hash_calls, prf_calls;
|
||||
static size_t hash_size, expected_seed;
|
||||
static void mbedtls_platform_zeroize(void *p, size_t n) { memset(p, 0, n); }
|
||||
static int verify(const mbedtls_ssl_context *ssl, unsigned char *out, size_t *len)
|
||||
{
|
||||
hash_calls++;
|
||||
if (hash_error) return hash_error; /* Deliberately leave seed_len=64. */
|
||||
*len = hash_size;
|
||||
memset(out, 0x23, *len);
|
||||
return 0;
|
||||
}
|
||||
static int prf(const unsigned char *p, size_t n, const char *label,
|
||||
const unsigned char *seed, size_t len, unsigned char *out, size_t size)
|
||||
{
|
||||
prf_calls++;
|
||||
assert(len == expected_seed && size == 48);
|
||||
assert(strcmp(label, len == 64 ? "master secret" : "extended master secret") == 0);
|
||||
for (size_t i = 0; i < len; i++) assert(seed[i] == (len == 64 ? 0x45 : 0x23));
|
||||
if (prf_error) return prf_error;
|
||||
memset(out, 0x67, size);
|
||||
return 0;
|
||||
}
|
||||
/* SDK_FUNCTIONS */
|
||||
int main(void)
|
||||
{
|
||||
mbedtls_ssl_context ssl = {0};
|
||||
for (unsigned sha = 0; sha < 2; sha++) {
|
||||
hash_size = sha ? 48 : 32;
|
||||
for (unsigned mode = 0; mode < 5; mode++) {
|
||||
mbedtls_ssl_handshake_params h = {.calc_verify = verify, .tls_prf = prf,
|
||||
.pmslen = 32, .extended_ms = mode != 3, .resume = mode == 4};
|
||||
memset(h.premaster, 0xab, sizeof(h.premaster));
|
||||
memset(h.randbytes, 0x45, sizeof(h.randbytes));
|
||||
unsigned char master[48]; memset(master, 0xcd, sizeof(master));
|
||||
hash_calls = prf_calls = 0;
|
||||
hash_error = mode == 0 ? -0x1234 : 0;
|
||||
prf_error = mode == 2 ? -0x2345 : 0;
|
||||
expected_seed = mode == 3 ? 64 : hash_size;
|
||||
int ret = ssl_compute_master(&h, master, &ssl);
|
||||
assert(ret == (mode == 0 ? hash_error : mode == 2 ? prf_error : 0));
|
||||
assert(hash_calls == (mode == 3 || mode == 4 ? 0 : 1));
|
||||
assert(prf_calls == (mode == 0 || mode == 4 ? 0 : 1));
|
||||
for (size_t i = 0; i < sizeof(master); i++)
|
||||
assert(master[i] == (mode == 1 || mode == 3 ? 0x67 : 0xcd));
|
||||
for (size_t i = 0; i < sizeof(h.premaster); i++)
|
||||
assert(h.premaster[i] == (mode == 1 || mode == 3 ? 0 : 0xab));
|
||||
}
|
||||
}
|
||||
puts("EMS extracted master calculation: SHA256/SHA384 error, success, PRF failure, non-EMS, resumption PASS");
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import argparse
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -91,8 +92,16 @@ def generator_tests(idf, work):
|
||||
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])
|
||||
notice = sdk.MODIFICATION_NOTICE.encode()
|
||||
assert derived.startswith(notice)
|
||||
assert derived.count(notice) == 1
|
||||
assert b"Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15" in notice
|
||||
assert derived[len(notice):].startswith(original[:original.index(b"*/") + 2])
|
||||
assert derived != original
|
||||
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], target="mbedtls"),
|
||||
{"idf": idf, "project": ROOT}), "invalid nested target")
|
||||
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[1], target="unknown"),
|
||||
{"idf": idf, "project": ROOT}), "invalid nested target")
|
||||
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")
|
||||
@@ -162,6 +171,26 @@ def extracted_tests(idf, binary, work):
|
||||
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")
|
||||
dhcp = texts["dhcpserver"]
|
||||
parser = extract(dhcp, "parse_options")
|
||||
upstream = parser.replace("parse_options(", "upstream_parse_options(", 1)
|
||||
upstream = upstream.replace("end - optptr < 2", "optptr + 1 >= end")
|
||||
upstream = upstream.replace("opt_len > end - optptr - 2", "optptr + 2 + opt_len > end")
|
||||
names = ("DHCP_OPTION_PAD", "DHCP_OPTION_END", "DHCP_OPTION_MSG_TYPE",
|
||||
"DHCP_OPTION_REQ_IPADDR", "DHCPDISCOVER", "DHCPREQUEST", "DHCPDECLINE", "DHCPRELEASE")
|
||||
defines = "\n".join(re.search(r"^#define " + name + r"\s+[^\n]+", dhcp, re.M).group() for name in names)
|
||||
source = (HERE / "dhcp.c").read_text().replace("/* SDK_DEFINES */", defines)
|
||||
compile_run("dhcp", source.replace("/* SDK_FUNCTIONS */", parser + upstream), work,
|
||||
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
|
||||
compile_run("ems", (HERE / "ems.c").read_text().replace("/* SDK_FUNCTIONS */",
|
||||
extract(texts["mbedtls_ssl_tls"], "ssl_compute_master")), work,
|
||||
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
|
||||
x509 = texts["mbedtls_x509_create"]
|
||||
functions = x509[x509.index("typedef struct {"):x509.index("int mbedtls_x509_string_to_names(")]
|
||||
functions += extract(x509, "mbedtls_x509_string_to_names")
|
||||
compile_run("x509", (HERE / "x509.c").read_text().replace("/* SDK_FUNCTIONS */", functions), work,
|
||||
["-I", str(idf / "components/mbedtls/mbedtls/include"),
|
||||
"-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
|
||||
|
||||
|
||||
def compile_run(name, source, work, flags=()):
|
||||
@@ -175,41 +204,86 @@ def compile_run(name, source, work, flags=()):
|
||||
def cmake_fixture_tests(idf, work):
|
||||
# Use real component inputs with mock IDF target discovery. No SDK compilation.
|
||||
fixture = work / "cmake_fixture"; fixture.mkdir()
|
||||
installed_idf = idf
|
||||
idf = fixture / "idf"
|
||||
for rel in ["components/esp_common/include/esp_idf_version.h"] + [e.source for e in sdk.ENTRIES if e.root == "idf"]:
|
||||
dest = idf / rel; dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(installed_idf / rel, dest)
|
||||
nested_dir = idf / "components/mbedtls/mbedtls/library"
|
||||
nested_lines = []
|
||||
lines = ["cmake_minimum_required(VERSION 3.18)", "project(security_fixture C)",
|
||||
'set(CMAKE_EXPORT_COMPILE_COMMANDS ON)',
|
||||
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()']
|
||||
targets = set()
|
||||
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)}")']
|
||||
target = e.target or f"test_{e.component}"
|
||||
owner_lines = nested_lines if e.target else lines
|
||||
registered_source = Path(e.source).name if e.target else source_path(e, idf, fixture)
|
||||
if target not in targets:
|
||||
owner_lines += [f'add_library({target} STATIC "{registered_source}")']
|
||||
targets.add(target)
|
||||
else:
|
||||
owner_lines += [f'target_sources({target} PRIVATE "{source_path(e, idf, fixture)}")']
|
||||
lines += ['add_library(test_mbedtls INTERFACE)', f'add_subdirectory("{nested_dir}" nested)']
|
||||
(nested_dir / "CMakeLists.txt").write_text(
|
||||
'if(NOT TEST_NESTED_MISSING AND NOT TEST_NESTED_OWNER)\n' + "\n".join(nested_lines) + '\nendif()\n')
|
||||
lines += ['if(TEST_NESTED_OWNER)',
|
||||
f'add_library(mbedtls STATIC "{source_path(sdk.ENTRIES[1], idf)}")', 'endif()']
|
||||
for target in ("mbedtls", "mbedx509"):
|
||||
entry = next(e for e in sdk.ENTRIES if e.target == target)
|
||||
lines += [f'if(TEST_{target}_MISSING_SOURCE)', f'set_property(TARGET {target} PROPERTY SOURCES missing.c)', 'endif()',
|
||||
f'if(TEST_{target}_DUPLICATE)', f'set_property(TARGET {target} APPEND PROPERTY SOURCES "{source_path(entry, idf)}")', 'endif()']
|
||||
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")']
|
||||
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
|
||||
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}"{directory} 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)',
|
||||
target = e.target or f"test_{e.component}"
|
||||
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
|
||||
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:{target},SOURCES>")',
|
||||
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} 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'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} 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)
|
||||
assert source.split(';').count(str(build / "security_overrides" / e.name / Path(e.source).name)) == 1
|
||||
assert str(source_path(e, idf, fixture)) not in source
|
||||
build_registration(build, idf, fixture)
|
||||
commands = json.loads((build / "compile_commands.json").read_text())
|
||||
for e in sdk.ENTRIES:
|
||||
generated = str(build / "security_overrides" / e.name / Path(e.source).name)
|
||||
matches = [c for c in commands if c["file"] == generated]
|
||||
assert len(matches) == 1, (e.name, matches)
|
||||
for option in ("-DSOURCE_FLAG", "-DSOURCE_DEFINE", "-fno-common",
|
||||
str(source_path(e, idf, fixture).parent)):
|
||||
assert option in matches[0]["command"], (e.name, option, matches)
|
||||
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")):
|
||||
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"),
|
||||
("TEST_TARGET_MISSING", "missing component target"),
|
||||
("TEST_NESTED_MISSING", "missing nested target"),
|
||||
("TEST_NESTED_OWNER", "unexpected nested target owner"),
|
||||
("TEST_mbedtls_MISSING_SOURCE", "found 0"),
|
||||
("TEST_mbedtls_DUPLICATE", "found 2"),
|
||||
("TEST_mbedx509_MISSING_SOURCE", "found 0"),
|
||||
("TEST_mbedx509_DUPLICATE", "found 2")):
|
||||
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")
|
||||
@@ -267,16 +341,19 @@ def extension_fixture_tests(idf, work):
|
||||
print("Extension mapping + child relative source/includes/flags real compile + automatic mismatch rejection PASS")
|
||||
|
||||
|
||||
def build_registration(build, idf):
|
||||
def build_registration(build, idf, project=ROOT):
|
||||
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")
|
||||
assert not any(str(source_path(e, idf, project)) in line for line in compile_lines), e.name
|
||||
target = e.target or (f"test_{e.component}" if project != ROOT else f"__idf_{e.component}")
|
||||
assert f"CMakeFiles/{target}.dir/" in matches[0], (e.name, matches)
|
||||
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": project})[1]
|
||||
kind = "Real IDF" if project == ROOT else "CMake fixture"
|
||||
print(f"{kind} Ninja registration: generated inputs once on exact owner, originals absent, bytes verified PASS")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "mbedtls/x509.h"
|
||||
#include "mbedtls/asn1write.h"
|
||||
#include "mbedtls/oid.h"
|
||||
static unsigned allocations, fail_at, live;
|
||||
static void *test_calloc(size_t n, size_t size)
|
||||
{
|
||||
if (++allocations == fail_at) return NULL;
|
||||
void *p = calloc(n, size); assert(p); live++; return p;
|
||||
}
|
||||
static void test_free(void *p) { if (p) { assert(live); live--; free(p); } }
|
||||
#define mbedtls_calloc test_calloc
|
||||
#define mbedtls_free test_free
|
||||
void mbedtls_asn1_free_named_data_list(mbedtls_asn1_named_data **head)
|
||||
{
|
||||
while (*head) {
|
||||
mbedtls_asn1_named_data *next = (*head)->next;
|
||||
test_free((*head)->oid.p); test_free((*head)->val.p); test_free(*head);
|
||||
*head = next;
|
||||
}
|
||||
}
|
||||
/* Storage double: preserve the real parser's allocation/copy/free sequence;
|
||||
* unrelated repeated-OID replacement behavior is deliberately not modeled. */
|
||||
mbedtls_asn1_named_data *mbedtls_asn1_store_named_data(mbedtls_asn1_named_data **head,
|
||||
const char *oid, size_t oid_len, const unsigned char *val, size_t val_len)
|
||||
{
|
||||
mbedtls_asn1_named_data *node = test_calloc(1, sizeof(*node));
|
||||
if (!node) return NULL;
|
||||
node->oid.p = test_calloc(1, oid_len);
|
||||
node->val.p = test_calloc(1, val_len);
|
||||
if (!node->oid.p || !node->val.p) {
|
||||
test_free(node->oid.p); test_free(node->val.p); test_free(node); return NULL;
|
||||
}
|
||||
memcpy(node->oid.p, oid, oid_len); node->oid.len = oid_len;
|
||||
memcpy(node->val.p, val, val_len); node->val.len = val_len;
|
||||
node->next = *head; *head = node; return node;
|
||||
}
|
||||
int mbedtls_oid_from_numeric_string(mbedtls_asn1_buf *oid, const char *p, size_t n)
|
||||
{ assert(!"numeric OID outside this focused test"); return -1; }
|
||||
int mbedtls_asn1_get_len(unsigned char **p, const unsigned char *end, size_t *len)
|
||||
{ assert(!"hex DER outside this focused test"); return -1; }
|
||||
/* SDK_FUNCTIONS */
|
||||
int main(void)
|
||||
{
|
||||
/* Subject and issuer each call this same parser; fail each allocation in
|
||||
* their separate lists, then destroy partial lists and retry from scratch. */
|
||||
for (unsigned failure = 1; failure <= 8; failure++) {
|
||||
mbedtls_asn1_named_data *subject = NULL, *issuer = NULL;
|
||||
allocations = 0; fail_at = failure;
|
||||
int ret = mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF");
|
||||
if (!ret) ret = mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF");
|
||||
assert(ret == MBEDTLS_ERR_X509_ALLOC_FAILED);
|
||||
mbedtls_asn1_free_named_data_list(&subject);
|
||||
mbedtls_asn1_free_named_data_list(&issuer);
|
||||
assert(live == 0);
|
||||
allocations = 0; fail_at = 0;
|
||||
assert(mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF") == 0);
|
||||
assert(mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF") == 0);
|
||||
assert(subject->val.len == strlen("ESP32 SAK ABCDEF"));
|
||||
assert(memcmp(subject->val.p, "ESP32 SAK ABCDEF", subject->val.len) == 0);
|
||||
assert(issuer->val.tag == MBEDTLS_ASN1_UTF8_STRING);
|
||||
mbedtls_asn1_free_named_data_list(&subject);
|
||||
mbedtls_asn1_free_named_data_list(&issuer);
|
||||
assert(live == 0);
|
||||
}
|
||||
mbedtls_asn1_named_data *head = NULL;
|
||||
allocations = 0; fail_at = 5; /* Second known-attribute OID, after a live node. */
|
||||
assert(mbedtls_x509_string_to_names(&head, "CN=first,O=second") == MBEDTLS_ERR_X509_ALLOC_FAILED);
|
||||
assert(head && head->val.len == 5);
|
||||
mbedtls_asn1_free_named_data_list(&head); assert(live == 0);
|
||||
puts("X509 extracted name parser/helpers: subject/issuer OOM, partial-list cleanup, successful retry PASS");
|
||||
}
|
||||
Reference in New Issue
Block a user