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:
@@ -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