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