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");
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# Bounded CVE-2025-12888 mitigation
|
||||
|
||||
Scope: project-owned build configuration only, retaining wolfSSL 5.8.2~1 and
|
||||
wolfSSH 1.4.20 pins and unmodified managed sources. This is not an upstream
|
||||
upgrade, blanket security clearance, or mitigation of other listed advisories.
|
||||
|
||||
## Upstream and installed evidence
|
||||
|
||||
On 2026-09-15, inspected official
|
||||
[PR9275 files](https://api.github.com/repos/wolfSSL/wolfssl/pulls/9275/files)
|
||||
([PR](https://github.com/wolfSSL/wolfssl/pull/9275), head reported by the files
|
||||
API: `c161cbd9f3fa1247382bb5b6269c7379222cabf5`). Its `settings.h` patch
|
||||
selects `CURVE25519_SMALL`, `ED25519_SMALL`, `CURVE448_SMALL`, and `ED448_SMALL`
|
||||
under `__xtensa__`: Xtensa compilers have generated non-constant-time assembly
|
||||
from the fast C implementation; upstream says the small implementation is not
|
||||
known to have those issues. This is upstream mitigation guidance, not proof of
|
||||
constant-time execution on our compiler/device.
|
||||
|
||||
Installed `include/user_settings.h` enables X25519 and Ed25519. Installed
|
||||
`wolfssl/wolfcrypt/settings.h` automatically enables X25519 blinding only for
|
||||
non-small math; `wolfcrypt/src/curve25519.c` rejects blinding with small math.
|
||||
`fe_low_mem.c` and `ge_low_mem.c` provide the small implementations and already
|
||||
have entries in the production compilation database. Small flags change public
|
||||
key layout/signatures: never mix old library objects with newly compiled callers.
|
||||
|
||||
Root `CMakeLists.txt` sets both small flags before component processing, alongside
|
||||
the existing global crypto controls. `cmake/wolf_crypto_policy.cmake` attaches a
|
||||
forced-include resolved-settings guard to wolfSSL with PUBLIC propagation to its
|
||||
consumers, including wolfSSH and application code. The guard rejects missing
|
||||
algorithms/small flags, incompatible blinding, and future 448 enablement pending
|
||||
explicit review. No blinding-disable macro or vendor source patch is needed.
|
||||
RNG callback and software AES/SHA settings remain unchanged.
|
||||
|
||||
## Commands
|
||||
|
||||
From the repository root, after the parent regenerates/builds the firmware:
|
||||
|
||||
```sh
|
||||
python3 tests/wolf_crypto_policy/run.py
|
||||
```
|
||||
|
||||
Optional explicit database:
|
||||
|
||||
```sh
|
||||
python3 tests/wolf_crypto_policy/run.py --compile-commands .pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json
|
||||
```
|
||||
|
||||
Strict mode requires the actual compile commands to carry the policy guard and
|
||||
uses their actual compiler, include paths and definitions without adding small
|
||||
flags. Missing/ambiguous entries, absent policy, wrong architecture, incompatible
|
||||
macros, compiler errors and failed vectors fail the test. It preprocesses and
|
||||
syntax-checks ten translation units: Curve25519, Ed25519, fast and small field/group
|
||||
math, wolfSSH `ssh.c`, generated wolfSSH `internal.c`, application transport and
|
||||
security. Two additional actual-settings checks remove each small flag and must
|
||||
fail. It does not modify generated sources or compile databases.
|
||||
|
||||
Before the parent reconfigures, explicitly test the candidate using old commands:
|
||||
|
||||
```sh
|
||||
python3 tests/wolf_crypto_policy/run.py --candidate
|
||||
```
|
||||
|
||||
This injects the two small flags and the guard and labels its output **CANDIDATE
|
||||
replay**, not production configuration evidence. It does not run CMake/PlatformIO.
|
||||
Host-only subset:
|
||||
|
||||
```sh
|
||||
python3 tests/wolf_crypto_policy/run.py --host-only
|
||||
```
|
||||
|
||||
All modes run eight guard matrix cases and compile the installed vendor small
|
||||
implementations into a temporary host executable: RFC7748 section 6.1 X25519
|
||||
shared secret, RFC8032 section 7.1 test 1 Ed25519 empty-message verification, and
|
||||
rejection of a corrupted signature. Host settings are deliberately minimal,
|
||||
with streaming verification enabled and unused functions garbage-collected;
|
||||
they are not the ESP-IDF runtime/entropy/hardware configuration. No synthetic
|
||||
implementation substitutes for the tested arithmetic. Requirements: Python 3,
|
||||
`cc`/linker, installed managed component; target checks also require the existing
|
||||
Xtensa toolchain, generated headers and compile database. Commands are bounded;
|
||||
temporary outputs are removed automatically.
|
||||
|
||||
## Validation and remaining gates
|
||||
|
||||
Implemented validation: candidate replay passed all ten macro/syntax checks,
|
||||
eight guard cases, two real-settings rejection cases, and the three host vector
|
||||
checks. Initial host harness compilation exposed a disabled SHA256 declaration
|
||||
dependency and omitted small-math source files; the harness was corrected to use
|
||||
the installed small source files explicitly.
|
||||
|
||||
The parent must run the normal full build and then strict mode above. A build was
|
||||
explicitly not run for this task. Existing compile-database success alone would
|
||||
not prove the linked/flashed image matches it. No device operations were run.
|
||||
Still required: target SSH X25519 negotiation, Ed25519 authentication, rekey,
|
||||
combined service load, stack/heap reserves and handshake latency/deadline checks.
|
||||
Small implementations may reduce performance; no target timing, side-channel
|
||||
measurement, interoperability or resource claim is made. Host vectors are narrow
|
||||
correctness checks, not exhaustive cryptographic validation.
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline compile-profile regression; never invokes PlatformIO or a device."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
GUARD = ROOT / 'cmake/wolf_crypto_policy.h'
|
||||
VENDOR = ROOT / 'managed_components/wolfssl__wolfssl'
|
||||
ENV = {**os.environ, 'CCACHE_DISABLE': '1'}
|
||||
SMALL = ('CURVE25519_SMALL', 'ED25519_SMALL')
|
||||
|
||||
|
||||
def run(args, cwd=ROOT, **kw):
|
||||
return subprocess.run(args, cwd=cwd, env=ENV, timeout=60,
|
||||
capture_output=True, text=True, **kw)
|
||||
|
||||
|
||||
def require(result):
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stderr)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def clean(entry):
|
||||
args = entry.get('arguments') or shlex.split(entry['command'])
|
||||
result = []
|
||||
skip = False
|
||||
for arg in args:
|
||||
if skip:
|
||||
skip = False
|
||||
elif arg in ('-o', '-MF', '-MT', '-MQ'):
|
||||
skip = True
|
||||
elif arg not in ('-c', '-MD', '-MMD', '-MP'):
|
||||
result.append(arg)
|
||||
return result
|
||||
|
||||
|
||||
def matrix():
|
||||
with tempfile.TemporaryDirectory(prefix='wolf-policy-') as tmp:
|
||||
tmp = Path(tmp)
|
||||
settings = tmp / 'wolfssl/wolfcrypt/settings.h'
|
||||
settings.parent.mkdir(parents=True)
|
||||
settings.write_text('/* Resolved settings supplied by matrix. */\n')
|
||||
base = ['HAVE_CURVE25519', 'HAVE_ED25519', *SMALL]
|
||||
cases = [('valid', base, True)]
|
||||
cases += [(f'missing {m}', [x for x in base if x != m], False) for m in base]
|
||||
cases += [(m, base + [m], False) for m in
|
||||
('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448')]
|
||||
for label, defines, good in cases:
|
||||
p = run(['cc', '-x', 'c', '-fsyntax-only', '-I' + str(tmp),
|
||||
'-include', str(GUARD), *['-D' + x for x in defines], '-'], input='')
|
||||
if (p.returncode == 0) != good or (not good and 'wolf crypto policy:' not in p.stderr):
|
||||
raise RuntimeError(f'guard matrix failed: {label}: {p.stderr}')
|
||||
print(f'PASS: {len(cases)} fail-closed guard cases')
|
||||
|
||||
|
||||
def profiles(database, candidate):
|
||||
entries = json.loads(database.read_text())
|
||||
suffixes = ('wolfcrypt/src/curve25519.c', 'wolfcrypt/src/ed25519.c',
|
||||
'wolfcrypt/src/fe_operations.c', 'wolfcrypt/src/ge_operations.c',
|
||||
'wolfcrypt/src/fe_low_mem.c', 'wolfcrypt/src/ge_low_mem.c',
|
||||
'wolfssl__wolfssh/src/ssh.c',
|
||||
'security_overrides/wolfssh_internal/internal.c',
|
||||
'src/ssh_transport.c', 'src/ssh_security.c')
|
||||
for suffix in suffixes:
|
||||
matches = [e for e in entries if e['file'].endswith('/' + suffix)]
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(f'expected one compile entry for {suffix}: {len(matches)}')
|
||||
entry = matches[0]
|
||||
command = clean(entry)
|
||||
if candidate:
|
||||
command += ['-D' + x for x in SMALL] + ['-include', str(GUARD)]
|
||||
elif not any('wolf_crypto_policy.h' in x for x in command):
|
||||
raise RuntimeError(f'{suffix}: missing production guard; parent must reconfigure/build')
|
||||
text = require(run(command + ['-E', '-dM'], cwd=entry['directory']))
|
||||
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', text, re.M))
|
||||
for name in (*SMALL, 'HAVE_CURVE25519', 'HAVE_ED25519',
|
||||
'WC_RNG_SEED_CB', 'WOLFSSL_ED25519_STREAMING_VERIFY',
|
||||
'NO_WOLFSSL_ESP32_CRYPT_AES', 'NO_WOLFSSL_ESP32_CRYPT_HASH'):
|
||||
if name not in macros:
|
||||
raise RuntimeError(f'{suffix}: missing resolved {name}')
|
||||
if not any(x in macros for x in ('__XTENSA__', '__xtensa__')):
|
||||
raise RuntimeError('expected actual Xtensa compiler')
|
||||
for name in ('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448'):
|
||||
if name in macros:
|
||||
raise RuntimeError(f'{suffix}: unexpected {name}')
|
||||
require(run(command + ['-fsyntax-only'], cwd=entry['directory']))
|
||||
print(f'PASS: {"CANDIDATE replay" if candidate else "production"} macros + syntax: {suffix}')
|
||||
# Exercise failures with the real installed settings, not only fake headers.
|
||||
entry = matches[0]
|
||||
base = [x for x in clean(entry) if x not in ['-D' + m for m in SMALL]
|
||||
and 'wolf_crypto_policy.h' not in x]
|
||||
for missing in SMALL:
|
||||
command = base + ['-D' + x for x in SMALL if x != missing]
|
||||
command += ['-U' + missing, '-include', str(GUARD), '-E']
|
||||
p = run(command, cwd=entry['directory'])
|
||||
if p.returncode == 0 or 'wolf crypto policy:' not in p.stderr:
|
||||
raise RuntimeError(f'real settings accepted missing {missing}: {p.stderr}')
|
||||
print('PASS: real target settings reject either missing small flag')
|
||||
|
||||
|
||||
def vectors():
|
||||
with tempfile.TemporaryDirectory(prefix='wolf-vectors-') as tmp:
|
||||
tmp = Path(tmp)
|
||||
(tmp / 'user_settings.h').write_text('''
|
||||
#define WOLFCRYPT_ONLY
|
||||
#define NO_ASN
|
||||
#define NO_RSA
|
||||
#define NO_DH
|
||||
#define NO_DSA
|
||||
#define NO_AES
|
||||
#define NO_DES3
|
||||
#define NO_RC4
|
||||
#define NO_MD4
|
||||
#define NO_MD5
|
||||
#define NO_SHA
|
||||
|
||||
#define NO_HMAC
|
||||
#define WC_NO_RNG
|
||||
#define NO_FILESYSTEM
|
||||
#define NO_WRITEV
|
||||
#define NO_DEV_RANDOM
|
||||
#define NO_MAIN_DRIVER
|
||||
#define HAVE_CURVE25519
|
||||
#define HAVE_ED25519
|
||||
#define CURVE25519_SMALL
|
||||
#define ED25519_SMALL
|
||||
#define WOLFSSL_SHA512
|
||||
#define WOLFSSL_ED25519_STREAMING_VERIFY
|
||||
''')
|
||||
sources = ['curve25519.c', 'ed25519.c', 'fe_operations.c',
|
||||
'ge_operations.c', 'fe_low_mem.c', 'ge_low_mem.c', 'sha512.c']
|
||||
require(run(['cc', '-std=c99', '-O2', '-DWOLFSSL_USER_SETTINGS',
|
||||
'-I' + str(tmp), '-I' + str(VENDOR), '-include', str(GUARD),
|
||||
'-ffunction-sections', '-fdata-sections',
|
||||
str(HERE / 'vectors.c'),
|
||||
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
|
||||
'-Wl,--gc-sections', '-o', str(tmp / 'vectors')]))
|
||||
print(require(run([str(tmp / 'vectors')])).strip())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--compile-commands', type=Path,
|
||||
default=ROOT / '.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json')
|
||||
parser.add_argument('--candidate', action='store_true',
|
||||
help='inject proposed flags/guard into old commands; NOT production evidence')
|
||||
parser.add_argument('--host-only', action='store_true')
|
||||
args = parser.parse_args()
|
||||
matrix()
|
||||
vectors()
|
||||
if not args.host_only:
|
||||
profiles(args.compile_commands, args.candidate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <wolfssl/wolfcrypt/curve25519.h>
|
||||
#include <wolfssl/wolfcrypt/ed25519.h>
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { \
|
||||
fprintf(stderr, "vector failure at line %d: %s\n", __LINE__, #x); return 1; \
|
||||
} } while (0)
|
||||
|
||||
static void unhex(const char *hex, unsigned char *out, unsigned int size)
|
||||
{
|
||||
for (unsigned int i = 0; i < size; ++i) {
|
||||
unsigned int value;
|
||||
if (sscanf(hex + 2 * i, "%2x", &value) != 1) return;
|
||||
out[i] = (unsigned char)value;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* RFC 7748 section 6.1: Alice private / Bob public / shared secret. */
|
||||
unsigned char secret[32], peer[32], expected[32], result[32];
|
||||
curve25519_key alice, bob;
|
||||
word32 size = sizeof(result);
|
||||
unhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", secret, 32);
|
||||
unhex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", peer, 32);
|
||||
unhex("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742", expected, 32);
|
||||
CHECK(wc_curve25519_init(&alice) == 0);
|
||||
CHECK(wc_curve25519_init(&bob) == 0);
|
||||
CHECK(wc_curve25519_import_private_ex(secret, 32, &alice, EC25519_LITTLE_ENDIAN) == 0);
|
||||
CHECK(wc_curve25519_import_public_ex(peer, 32, &bob, EC25519_LITTLE_ENDIAN) == 0);
|
||||
CHECK(wc_curve25519_shared_secret_ex(&alice, &bob, result, &size, EC25519_LITTLE_ENDIAN) == 0);
|
||||
CHECK(size == 32 && memcmp(result, expected, 32) == 0);
|
||||
wc_curve25519_free(&alice);
|
||||
wc_curve25519_free(&bob);
|
||||
|
||||
/* RFC 8032 section 7.1 test 1: empty-message signature. */
|
||||
ed25519_key key;
|
||||
unsigned char public_key[32], signature[64];
|
||||
int valid = 0;
|
||||
unhex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", public_key, 32);
|
||||
unhex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
|
||||
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b", signature, 64);
|
||||
CHECK(wc_ed25519_init(&key) == 0);
|
||||
CHECK(wc_ed25519_import_public(public_key, 32, &key) == 0);
|
||||
CHECK(wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key) == 0);
|
||||
CHECK(valid == 1);
|
||||
signature[0] ^= 1;
|
||||
valid = 0;
|
||||
(void)wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key);
|
||||
CHECK(valid == 0);
|
||||
wc_ed25519_free(&key);
|
||||
puts("PASS: host installed small math RFC7748 X25519 / RFC8032 Ed25519 + bad signature");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user