Harden wolfSSL and wolfSSH validation

Enable validated ECC imports and X25519 all-zero rejection through
PUBLIC build policy. Tighten wolfSSH parser bounds, overflow handling,
and signature framing with guard-page and crypto vector contracts.
This commit is contained in:
2026-09-15 23:54:39 +02:00
parent c010e1a1d5
commit 4d3bb490c9
17 changed files with 1495 additions and 99 deletions
+91 -74
View File
@@ -1,97 +1,114 @@
# Bounded CVE-2025-12888 mitigation
# Bounded wolf crypto compile policy
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.
Project-owned configuration for pinned wolfSSL 5.8.2~1 / wolfSSH 1.4.20;
no installed vendor edits, dependency upgrades, generated override edits or
blanket security clearance. See the [key-validation review](../../docs/ssh_key_validation_review.md)
for exact source hashes, applicability, upstream guidance and remaining gaps.
## Upstream and installed evidence
## Policy
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.
- Existing root definitions `CURVE25519_SMALL` / `ED25519_SMALL` follow
[PR9275](https://github.com/wolfSSL/wolfssl/pull/9275)'s Xtensa mitigation.
Small math is incompatible with this version's X25519 blinding; do not mix
ABI-sensitive library and consumer settings. Curve448/Ed448 require review.
- `cmake/wolf_crypto_policy.cmake` now PUBLIC-defines
`WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO`, enabling
existing upstream P256 import and X25519 result checks. The root already
includes this module; no root edit is needed.
- The PUBLIC forced-include guard checks resolved settings and rejects missing
requirements and known ECC validator-disabling/hardware-stub configurations.
Existing RNG callback and software AES/SHA controls are unchanged.
## Commands
From the repository root, after the parent regenerates/builds the firmware:
```sh
# Offline host subset; no target compiler/database required:
python3 tests/wolf_crypto_policy/run.py --host-only
# Explicit candidate replay before production reconfiguration:
python3 tests/wolf_crypto_policy/run.py --candidate
# Strict production evidence after the parent reconfigures/builds:
python3 tests/wolf_crypto_policy/run.py
```
Optional explicit database:
Optional database argument:
```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.
Candidate mode injects all four policy definitions and the guard into saved
commands. It is **not production configuration/build evidence**. Strict mode
injects nothing, and must fail with stale commands lacking the new flags.
No mode runs PlatformIO, regenerates overrides or communicates with a device.
Before the parent reconfigures, explicitly test the candidate using old commands:
## Coverage
```sh
python3 tests/wolf_crypto_policy/run.py --candidate
```
All modes:
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:
- 20 fail-closed guard matrix cases.
- A temporary CMake project includes the actual production policy module and
verifies PUBLIC definitions/guard across wolf library → SSH → app targets.
This is a stand-in graph, not an ESP-IDF build.
- Compile installed small implementations and run RFC7748 X25519 and RFC8032
Ed25519 vectors plus corrupted-signature rejection. Check two nontrivial
low-order X25519 points that pass the vendor public precheck but must fail
shared-secret calculation without copying output; also reject zero/one inputs.
- Compile installed TFM ECC, ASN template, signature and supporting primitives.
Test explicit/inferred P256 import of valid G and rejection of off-curve,
infinity, out-of-range, truncated and wrong-tag inputs; valid ECDH; raw and
DER-wrapper ECDSA valid/invalid verification; valid SEC1 private DER decoding
without a pre-attached RNG and rejection of an invalid embedded public point.
Test scalar/nonce values are deliberately public test values, never real keys.
```sh
python3 tests/wolf_crypto_policy/run.py --host-only
```
Target modes additionally:
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.
- Pin original wolfSSH `internal.c` and wolfCrypt `ecc.c`, `curve25519.c`,
`signature.c`; locate the actual generated wolfSSH compile input and compare
seven complete audited crypto/auth/hash function bodies: five must remain
identical, while ECC/Ed25519 authentication must match independently specified
exact label/framing deltas reconstructed from the hash-pinned original, with
exact anchor counts. Expectations are not imported from the generator. Any
additional change requires re-audit, not repinning or skipping a body. This
does not validate the entire override generator. Generated hash is printed.
- Replay actual Xtensa compiler/includes for macro and syntax checks of twelve
translation units: ECC, signature wrapper, Curve25519, Ed25519, fast/small
field/group math, wolfSSH `ssh.c`, generated `internal.c`, application SSH
transport and security. Confirm internal `HAVE_ECC_CHECK_PUBKEY_ORDER` in ECC.
- Four negative actual-settings tests remove one policy flag at a time.
## Validation and remaining gates
Requirements: Python 3, CMake, host `cc`/linker, installed managed sources;
target modes also require the existing Xtensa toolchain, generated headers and
compilation database. Subprocesses have time bounds and temporary artifacts are
removed. No replacement crypto implementation or crypto success double is used.
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.
## Evidence and remaining gates — 2026-09-15
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.
Follow-up strict production run PASS without candidate injection, including all
host tests (20 guard cases, three-target CMake propagation, real crypto and ASN
vectors), seven complete source-body comparisons with reviewed exact parser
deltas, twelve target macro/syntax checks and four negative target-settings
cases. Earlier candidate and host-only runs also passed; the final ASN-decode
cases passed in candidate and strict runs.
Earlier development runs required correcting fixture settings/linkage; they
are not additional production failures. Host settings retain TFM, timing
resistance and small-stack allocation for ECC, but differ in word size,
allocator, OS entropy and hardware/compiler configuration. No sanitizer,
exhaustive fuzzing, allocation-failure injection or timing result is claimed.
The parent reports `pio run` PASS: 94,340 B linked RAM / 1,768,949 B flash.
This agent did not run PlatformIO or devices; local strict checks validate the
saved production compile profile, not a flashed image. Hardware tests remain
necessary for both KEX algorithms, P256/Ed25519 authentication, host-key loading,
rekey, malformed-key failure/cleanup, combined load, stack/heap reserves and
handshake deadlines. Extra import validation has real CPU/allocation cost.
The parser owner separately fixed ECC/Ed25519 labels, ECC nested exact bounds
and Ed25519 outer consumption in the generated input. Those changes are checked
by this suite's exact source contract, not supplied by crypto compile flags.
The parser suite was reviewed, not rerun in this follow-up; its crypto doubles
establish parser gating, not real signature arithmetic. Broader ordering/state
review, standalone ECC key-blob semantics outside application checks, generic
wolfSSL digest/OID API hardening as applicable, and hardware gates remain open;
see the review for evidence and limits.
+124
View File
@@ -0,0 +1,124 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <stdio.h>
#include <string.h>
#include <wolfssl/wolfcrypt/ecc.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssl/wolfcrypt/asn_public.h>
#include <wolfssl/wolfcrypt/signature.h>
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "ECC failure at line %d: %s\n", __LINE__, #x); return 1; \
} } while (0)
static void unhex(const char *hex, byte *out, unsigned int size)
{
for (unsigned int i = 0; i < size; ++i) {
unsigned int value = 0;
(void)sscanf(hex + 2 * i, "%2x", &value);
out[i] = (byte)value;
}
}
static int import_point(const byte *point, word32 size, int explicit_curve)
{
ecc_key key;
int ret = wc_ecc_init(&key);
if (ret != 0) return ret;
ret = explicit_curve ? wc_ecc_import_x963_ex(point, size, &key, ECC_SECP256R1)
: wc_ecc_import_x963(point, size, &key);
wc_ecc_free(&key);
return ret;
}
int main(void)
{
byte generator[65], bad[65], scalar[32] = {0}, secret[32], hash[32] = {0};
ecc_key private_key, public_key;
WC_RNG rng;
word32 size = sizeof(secret);
mp_int r, s;
int valid;
generator[0] = 4;
unhex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"
"4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5",
generator + 1, 64);
for (int explicit_curve = 0; explicit_curve <= 1; ++explicit_curve) {
CHECK(import_point(generator, sizeof(generator), explicit_curve) == 0);
memcpy(bad, generator, sizeof(bad));
bad[64] ^= 1;
CHECK(import_point(bad, sizeof(bad), explicit_curve) != 0);
memset(bad, 0, sizeof(bad));
bad[0] = 4;
CHECK(import_point(bad, sizeof(bad), explicit_curve) != 0);
memcpy(bad, generator, sizeof(bad));
unhex("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", bad + 1, 32);
CHECK(import_point(bad, sizeof(bad), explicit_curve) != 0);
CHECK(import_point(generator, 64, explicit_curve) != 0);
bad[0] = 5;
CHECK(import_point(bad, sizeof(bad), explicit_curve) != 0);
}
CHECK(wc_InitRng(&rng) == 0);
CHECK(wc_ecc_init(&private_key) == 0);
CHECK(wc_ecc_init(&public_key) == 0);
scalar[31] = 1;
/* Match wolfSSH's private-key decode: initially no attached RNG. */
int import_ret = wc_ecc_import_private_key_ex(scalar, sizeof(scalar), generator,
sizeof(generator), &private_key, ECC_SECP256R1);
if (import_ret != 0) fprintf(stderr, "private import returned %d\n", import_ret);
CHECK(import_ret == 0);
CHECK(wc_ecc_set_rng(&private_key, &rng) == 0);
CHECK(wc_ecc_import_x963_ex(generator, sizeof(generator), &public_key, ECC_SECP256R1) == 0);
CHECK(wc_ecc_shared_secret(&private_key, &public_key, secret, &size) == 0);
CHECK(size == 32 && memcmp(secret, generator + 1, 32) == 0);
/* Algebraic test only: d=k=z=1 gives r=Gx, s=(1+r) mod n.
* Never use these deliberately public scalars for real signing. */
CHECK(mp_init(&r) == 0);
CHECK(mp_init(&s) == 0);
CHECK(mp_read_unsigned_bin(&r, generator + 1, 32) == 0);
CHECK(mp_add_d(&r, 1, &s) == 0);
hash[31] = 1;
valid = 0;
CHECK(wc_ecc_verify_hash_ex(&r, &s, hash, sizeof(hash), &valid, &public_key) == 0);
CHECK(valid == 1);
hash[31] = 2;
valid = 0;
CHECK(wc_ecc_verify_hash_ex(&r, &s, hash, sizeof(hash), &valid, &public_key) == 0);
CHECK(valid == 0);
/* Exercise the DER signature wrapper used by wolfSSH as well. */
byte raw_s[32], signature[80];
word32 signature_size = sizeof(signature);
CHECK(mp_to_unsigned_bin(&s, raw_s) == 0);
CHECK(wc_ecc_rs_raw_to_sig(generator + 1, 32, raw_s, 32,
signature, &signature_size) == 0);
hash[31] = 1;
CHECK(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, WC_SIGNATURE_TYPE_ECC,
hash, sizeof(hash), signature, signature_size, &public_key, sizeof(public_key)) == 0);
hash[31] = 2;
CHECK(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, WC_SIGNATURE_TYPE_ECC,
hash, sizeof(hash), signature, signature_size, &public_key, sizeof(public_key)) != 0);
mp_clear(&r);
mp_clear(&s);
wc_ecc_free(&private_key);
wc_ecc_free(&public_key);
/* SEC1 ECPrivateKey with named P256, d=1, public G. Exercise the same ASN
* import used for host-key identification and per-handshake loading. */
byte der[121] = {0x30, 0x77, 0x02, 0x01, 0x01, 0x04, 0x20};
const byte suffix[] = {0xa0, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce,
0x3d, 0x03, 0x01, 0x07, 0xa1, 0x44, 0x03, 0x42, 0x00};
memcpy(der + 7, scalar, 32);
memcpy(der + 39, suffix, sizeof(suffix));
memcpy(der + 56, generator, sizeof(generator));
for (int corrupt = 0; corrupt < 2; ++corrupt) {
word32 index = 0;
CHECK(wc_ecc_init(&private_key) == 0);
if (corrupt) der[120] ^= 1;
int ret = wc_EccPrivateKeyDecode(der, &index, &private_key, sizeof(der));
CHECK(corrupt ? ret != 0 : ret == 0);
wc_ecc_free(&private_key);
}
wc_FreeRng(&rng);
puts("PASS: vendor TFM P256 import/invalid points, ECDH, raw/DER ECDSA, private ASN decode");
return 0;
}
+188 -11
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Offline compile-profile regression; never invokes PlatformIO or a device."""
import argparse
import hashlib
import json
import os
from pathlib import Path
@@ -15,6 +16,11 @@ GUARD = ROOT / 'cmake/wolf_crypto_policy.h'
VENDOR = ROOT / 'managed_components/wolfssl__wolfssl'
ENV = {**os.environ, 'CCACHE_DISABLE': '1'}
SMALL = ('CURVE25519_SMALL', 'ED25519_SMALL')
VALIDATION = ('WOLFSSL_VALIDATE_ECC_IMPORT', 'WOLFSSL_ECDHX_SHARED_NOT_ZERO')
POLICY = (*SMALL, *VALIDATION)
ECC_BACKENDS = ('NO_ECC_CHECK_PUBKEY_ORDER', 'WOLF_CRYPTO_CB_ONLY_ECC',
'WOLFSSL_ATECC508A', 'WOLFSSL_ATECC608A', 'WOLFSSL_CRYPTOCELL',
'WOLFSSL_SILABS_SE_ACCEL', 'WOLFSSL_SE050', 'WOLFSSL_STM32_PKA')
def run(args, cwd=ROOT, **kw):
@@ -48,11 +54,12 @@ def matrix():
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]
base = ['HAVE_CURVE25519', 'HAVE_ED25519', 'HAVE_ECC',
'HAVE_ECC_CHECK_KEY', *POLICY]
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')]
('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448', *ECC_BACKENDS)]
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='')
@@ -61,11 +68,145 @@ def matrix():
print(f'PASS: {len(cases)} fail-closed guard cases')
def cmake_propagation():
"""Use the real policy module with a tiny stand-in IDF target graph."""
with tempfile.TemporaryDirectory(prefix='wolf-cmake-') as tmp:
tmp = Path(tmp)
settings = tmp / 'wolfssl/wolfcrypt/settings.h'
settings.parent.mkdir(parents=True)
settings.write_text('\n'.join('#define ' + m for m in
('HAVE_ECC', 'HAVE_ECC_CHECK_KEY', 'HAVE_CURVE25519',
'HAVE_ED25519', *SMALL)) + '\n')
(tmp / 'unit.c').write_text('int main(void) { return 0; }\n')
(tmp / 'CMakeLists.txt').write_text(f'''
cmake_minimum_required(VERSION 3.16)
project(wolf_policy_propagation C)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_library(wolf STATIC unit.c)
target_include_directories(wolf PUBLIC "{tmp}")
function(idf_component_get_property out component prop)
set(${{out}} wolf PARENT_SCOPE)
endfunction()
include("{ROOT / 'cmake/wolf_crypto_policy.cmake'}")
add_library(ssh STATIC unit.c)
target_link_libraries(ssh PUBLIC wolf)
add_executable(app unit.c)
target_link_libraries(app PRIVATE ssh)
''')
require(run(['cmake', '-S', str(tmp), '-B', str(tmp / 'build')]))
require(run(['cmake', '--build', str(tmp / 'build')]))
entries = json.loads((tmp / 'build/compile_commands.json').read_text())
if len(entries) != 3:
raise RuntimeError('CMake policy fixture must compile library, SSH, app')
for entry in entries:
args = clean(entry)
if not all('-D' + flag in args for flag in VALIDATION):
raise RuntimeError('validation definitions failed PUBLIC propagation')
if not any('wolf_crypto_policy.h' in arg for arg in args):
raise RuntimeError('guard failed PUBLIC propagation')
print('PASS: real CMake module PUBLIC definitions/guard across three targets')
def reviewed_ssh_body(name, body):
"""Independent allowlist of the reviewed parser delta, not generator output.
Start from the hash-pinned original and require exact occurrence counts;
the caller then compares the entire resulting function to the compiled file.
"""
def replace(old, new, count=1):
nonlocal body
if body.count(old) != count:
raise RuntimeError(f're-audit original parser anchor: {name}')
body = body.replace(old, new)
if name in ('DoUserAuthRequestEcc', 'DoUserAuthRequestEd25519'):
replace('if (publicKeyTypeSz != pk->publicKeyTypeSz &&\n',
'if (publicKeyTypeSz != pk->publicKeyTypeSz ||\n',
2 if name == 'DoUserAuthRequestEcc' else 1)
if name == 'DoUserAuthRequestEcc':
replace(''' if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&sSz, &s, pk->signature, pk->signatureSz, &i);
}
''', ''' if (ret == WS_SUCCESS) {
/* GetSize bounded sz by signatureSz - i: this end cannot wrap. */
sz += i;
ret = GetStringRef(&rSz, &r, pk->signature, sz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&sSz, &s, pk->signature, sz, &i);
}
if (ret == WS_SUCCESS && (i != sz || sz != pk->signatureSz))
ret = WS_BUFFER_E;
''')
elif name == 'DoUserAuthRequestEd25519':
replace('''if (publicKeyTypeSz != pk->publicKeyTypeSz
&& WMEMCMP(publicKeyType,''',
'''if (publicKeyTypeSz != pk->publicKeyTypeSz
|| WMEMCMP(publicKeyType,''')
replace(''' if (ret == WS_SUCCESS) {
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,''',
''' /* The signature string must consume the enclosing signature field. */
if (ret == WS_SUCCESS && sz != pk->signatureSz - i)
ret = WS_BUFFER_E;
if (ret == WS_SUCCESS) {
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,''')
return body
def source_contract(database):
pins = {
ROOT / 'managed_components/wolfssl__wolfssh/src/internal.c':
'81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9',
VENDOR / 'wolfcrypt/src/ecc.c':
'909c57e2756a8002df9f1d214483c659cb20db4a5f51047eda62d64ac458db06',
VENDOR / 'wolfcrypt/src/curve25519.c':
'9a0f6f0205245a8d19500a936d9b02bb71c8656713648408d1cb408362694b76',
VENDOR / 'wolfcrypt/src/signature.c':
'62ab3db3dfd251b2a2c73b69ef05aab6085d2e0d673fd9159514b3ee261cea4f',
}
for path, expected in pins.items():
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
raise RuntimeError(f're-audit key-validation source: {path}')
entries = json.loads(database.read_text())
entries = [e for e in entries if e['file'].endswith(
'/security_overrides/wolfssh_internal/internal.c')]
if len(entries) != 1:
raise RuntimeError('expected one generated wolfSSH compilation input')
generated = Path(entries[0]['file'])
if not generated.is_absolute():
generated = Path(entries[0]['directory']) / generated
original = next(iter(pins)).read_text()
derived = generated.read_text()
for name in ('HashForId', 'KeyAgreeEcdh_server', 'KeyAgreeCurve25519_server',
'SignHEcdsa', 'DoUserAuthRequestEcc', 'DoUserAuthRequestEd25519',
'DoUserAuthRequestPublicKey'):
pattern = rf'^(?:static )?(?:int|enum wc_HashType) {name}\('
def extract(text):
match = re.search(pattern, text, re.M)
if match is None:
raise RuntimeError(f'missing audited function {name}')
end = text.index('\n}\n', match.start()) + 3
return text[match.start():end]
expected = reviewed_ssh_body(name, extract(original))
if expected != extract(derived):
raise RuntimeError(f're-audit modified generated crypto path: {name}')
print('PASS: pinned originals; seven exact SSH paths including reviewed ECC/Ed parser deltas')
print('INFO: generated wolfSSH SHA256 ' + hashlib.sha256(generated.read_bytes()).hexdigest())
def profiles(database, candidate):
entries = json.loads(database.read_text())
suffixes = ('wolfcrypt/src/curve25519.c', 'wolfcrypt/src/ed25519.c',
suffixes = ('wolfcrypt/src/ecc.c', 'wolfcrypt/src/signature.c',
'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',
'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')
@@ -76,34 +217,38 @@ def profiles(database, candidate):
entry = matches[0]
command = clean(entry)
if candidate:
command += ['-D' + x for x in SMALL] + ['-include', str(GUARD)]
command += ['-D' + x for x in POLICY] + ['-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',
for name in (*POLICY, 'HAVE_ECC', 'HAVE_ECC_CHECK_KEY',
'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 suffix == 'wolfcrypt/src/ecc.c' and 'HAVE_ECC_CHECK_PUBKEY_ORDER' not in macros:
raise RuntimeError('ECC import validator has no software point check')
for name in ('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448',
*ECC_BACKENDS):
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]
base = [x for x in clean(entry) if x not in ['-D' + m for m in POLICY]
and 'wolf_crypto_policy.h' not in x]
for missing in SMALL:
command = base + ['-D' + x for x in SMALL if x != missing]
for missing in POLICY:
command = base + ['-D' + x for x in POLICY 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')
print('PASS: real target settings reject each missing policy flag')
def vectors():
@@ -128,6 +273,9 @@ def vectors():
#define NO_WRITEV
#define NO_DEV_RANDOM
#define NO_MAIN_DRIVER
#define HAVE_ECC
#define WOLFSSL_VALIDATE_ECC_IMPORT
#define WOLFSSL_ECDHX_SHARED_NOT_ZERO
#define HAVE_CURVE25519
#define HAVE_ED25519
#define CURVE25519_SMALL
@@ -144,6 +292,33 @@ def vectors():
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
'-Wl,--gc-sections', '-o', str(tmp / 'vectors')]))
print(require(run([str(tmp / 'vectors')])).strip())
settings = tmp / 'user_settings.h'
settings.write_text(settings.read_text().replace('#define WC_NO_RNG', '')
.replace('#define NO_DEV_RANDOM', '')
.replace('#define NO_FILESYSTEM', '')
.replace('#define NO_ASN', '') + '''
#define WOLFSSL_ASN_TEMPLATE
#define NO_CERTS
#define NO_PWDBASED
#define NO_PKCS12
#define USE_FAST_MATH
#define TFM_NO_ASM
#define TFM_TIMING_RESISTANT
#include <strings.h>
#define WOLFSSL_SMALL_STACK
#define ECC_TIMING_RESISTANT
#define NO_ECC_SIGN
#define SINGLE_THREADED
''')
sources = ['ecc.c', 'tfm.c', 'wolfmath.c', 'random.c', 'sha256.c',
'sha512.c', 'memory.c', 'asn.c', 'hash.c', 'coding.c', 'signature.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 / 'ecc_vectors.c'),
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
'-Wl,--gc-sections', '-o', str(tmp / 'ecc_vectors')]))
print(require(run([str(tmp / 'ecc_vectors')])).strip())
def main():
@@ -155,8 +330,10 @@ def main():
parser.add_argument('--host-only', action='store_true')
args = parser.parse_args()
matrix()
cmake_propagation()
vectors()
if not args.host_only:
source_contract(args.compile_commands)
profiles(args.compile_commands, args.candidate)
+22 -1
View File
@@ -3,6 +3,7 @@
#include <string.h>
#include <wolfssl/wolfcrypt/curve25519.h>
#include <wolfssl/wolfcrypt/ed25519.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "vector failure at line %d: %s\n", __LINE__, #x); return 1; \
@@ -32,6 +33,26 @@ int main(void)
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);
/* Two nontrivial low-order u-coordinates pass wolfSSH's public precheck.
* The scalar multiplication result, not just the input, must be checked. */
const char *low_order[] = {
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157"
};
for (unsigned int i = 0; i < 2; ++i) {
unhex(low_order[i], peer, 32);
CHECK(wc_curve25519_check_public(peer, 32, EC25519_LITTLE_ENDIAN) == 0);
CHECK(wc_curve25519_import_public_ex(peer, 32, &bob, EC25519_LITTLE_ENDIAN) == 0);
memset(result, 0xa5, sizeof(result));
size = sizeof(result);
CHECK(wc_curve25519_shared_secret_ex(&alice, &bob, result, &size,
EC25519_LITTLE_ENDIAN) == ECC_OUT_OF_RANGE_E);
for (unsigned int j = 0; j < sizeof(result); ++j) CHECK(result[j] == 0xa5);
}
memset(peer, 0, sizeof(peer));
CHECK(wc_curve25519_check_public(peer, 32, EC25519_LITTLE_ENDIAN) != 0);
peer[0] = 1;
CHECK(wc_curve25519_check_public(peer, 32, EC25519_LITTLE_ENDIAN) != 0);
wc_curve25519_free(&alice);
wc_curve25519_free(&bob);
@@ -51,6 +72,6 @@ int main(void)
(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");
puts("PASS: vendor small math RFC7748 / RFC8032, bad signature, X25519 low-order rejection");
return 0;
}
+130
View File
@@ -0,0 +1,130 @@
# Bounded wolfSSH parser contract
Run from the project root (installed pinned sources and a host C compiler required):
```sh
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
```
No download, PlatformIO, managed-component edit, production build-tree regeneration,
or device operation is performed. The runner verifies the original `internal.c`
SHA-256, calls the production `render_entry`, writes and reads back its generated
bytes in a temporary directory, and extracts complete actual functions. C tests
run with guard pages and UBSan trap instrumentation, both with and without
`WOLFSSH_SMALL_STACK`. The existing SDK suite separately tests generation and
CMake source replacement using fixtures. This is **not** a claim that an existing
production generated file or firmware binary contains these edits.
## Reviewed upstream evidence and exact implementation scope
Official diffs fetched and inspected on 2026-09-15:
- https://github.com/wolfSSL/wolfssh/pull/892.diff
- https://github.com/wolfSSL/wolfssh/pull/881.diff
- https://github.com/wolfSSL/wolfssh/pull/899.diff
- https://github.com/wolfSSL/wolfssh/pull/880.diff
These are PR URLs, not immutable commit pins. The authoritative local inputs remain
original wolfSSH **1.4.20**, SHA-256
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`,
plus the exact-once checked-in edits in `tools/security_overrides.py`. No repinning
or wholesale upstream patch application occurs.
Covered:
- **892 subset:** `DoIgnore` calls `GetSkip`; `GetSkip` uses `GetSize` and accepts
an empty string ending exactly at the payload boundary. `DoServiceRequest`
validates the full string with `GetSize` before `GetString`, retaining the old
strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior.
Failure leaves the caller index and client state untouched. Successful state
transition remains exactly the old one; service-name semantic validation is
not added. The original `GetSize` already uses bounded subtraction and needs
no change. `GetString` now uses it and rejects zero output capacity before
subtraction/copy; ordinary bounded truncation semantics remain unchanged.
- **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit
maximum with `WS_OVERFLOW_E`, leaving the channel window unchanged. The parsed
index still advances, as upstream does. No new include is needed for the
explicit word32 maximum. Unknown channels and truncated fields stay rejected.
- **880 subset:** both key/signature type checks in `DoUserAuthRequestEcc` and
`DoUserAuthRequestEd25519` use OR.
`GetSize` bounds the incoming span first; unequal lengths short-circuit before
`memcmp`, and equal lengths compare exactly the expected span. Valid matching
ECDSA/Ed25519 types follow the original crypto path. Existing error normalization
remains (`WS_CRYPTO_FAILED` for key parsing, `WS_INVALID_ALGO_ID` for signature
type mismatch).
- **Local signature-framing correction:** the ECC r/s parser uses the checked
end of the declared signature sub-blob, not the enclosing field size. The
preceding `GetSize` establishes `sz <= signatureSz - i`, so calculating that
end cannot wrap. Both mpints must exactly consume the sub-blob, and the
sub-blob must exactly consume the enclosing signature field. Ed25519 likewise
rejects bytes outside its declared signature string before starting message
verification. These framing errors return `WS_BUFFER_E`. This intentionally
rejects previously tolerated malformed trailing bytes; valid SSH signature
framing and the crypto calls/digest/message construction are unchanged.
This is a local correction verified against the pinned implementation and
[key-validation review](../../docs/ssh_key_validation_review.md), not a claim
that these framing edits came from PR 880.
Reachability evidence: the pinned `DoPacket` dispatches IGNORE, SERVICE_REQUEST,
and CHANNEL_WINDOW_ADJUST to these handlers. `DoUserAuthRequestPublicKey` calls
`DoUserAuthRequestEcc`/`DoUserAuthRequestEd25519` for ECDSA/Ed25519 authentication,
both enabled in this server's reviewed profile. Advertisement is not treated as a parser dispatch filter.
## Explicitly deferred (not fixed by this slice)
- **892:** client `DoServiceAccept`, agent key preparation, daemon authentication,
Windows terminal changes. Password framing/wiping is the existing local
correction, intentionally not replaced with upstream's later formulation.
- **899:** no hunks applied. `ParseRSAPubKey`/`ParseECCPubKey` skips require separate
client/KEX reachability analysis (not the server's `DoUserAuthRequestEcc`).
The old `DoChannelFailure` does not parse a channel ID at all; changing only its
`len != 0` typo would not establish a bounded channel-ID parser. Its existing
behavior is left unchanged rather than claiming the later parser contract.
Windows port/terminal hunks are out of scope.
- **880:** certificate RSA, agent, daemon, terminal, TPM and SCP changes are not
applied. No complete PR-880 closure is claimed.
- Message ordering/state machine (including CVE-2025-14942), service semantics,
standalone ECC curve-name/key-blob semantic validation, other parsers, client
behavior and broader crypto advisories are outside this slice. ECC point/import
validation belongs to the separate crypto-policy owner and is not changed here.
Existing account/key authorization, numeric r/s validity and Ed25519 raw
signature-size/crypto validity checks remain owned by their existing layers.
## Test boundaries
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid
and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary
pairs, unknown channels, ECC equal-length mismatches, shorter/longer matching
prefixes, empty types and every key/signature truncation. Expected ECC and
Ed25519 type bytes end at a protected page, testing unequal-length short-circuit
safety. Crypto and
channel lookup are doubles; tests establish parser gating, not real signature or
point validation. Numeric errors, context/channel layouts and name capacity are
host doubles, not production ABI verification.
The follow-up matrix in `auth_framing.c` covers every truncated ECC sub-blob
boundary with a complete r/s pair still available beyond that boundary, oversized
and wrapping nested lengths, malformed r/s lengths, inner/outer trailing bytes,
and physically guard-page-ended frames. Instrumented `ato32` also asserts that
nested length reads cannot use accessible bytes outside the sub-blob. Signature
input and surrounding canaries stay unchanged. Valid 32-byte and sign-padded
33-byte r/s encodings reach conversion with their bytes/lengths intact. Ed25519
covers both labels, all truncations, shortened/oversized/wrapping/trailing
signature strings, exact raw-signature forwarding and unchanged streamed message
bytes. Both paths retain crypto rejection behavior using doubles.
Validation: **3,124 cases per stack mode** (both pass with UBSan trap mode), plus
**six guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, and Ed25519 exact consumption. The
mutation copies exist only in temporary test files; core dumps are disabled for
those intentionally failing runs. These are framing-valid fixtures with crypto
doubles, not independently verified real signatures.
The runner also compares complete password, packet dispatch, public-key dispatch and selected deferred
functions against the pre-slice generated baseline to fence accidental changes.
The separate auth suite executes its 135 password/control-flow cases, including
payload wipe, callback framing and asynchronous pending retention. No whole-library
fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off
is implied.
@@ -0,0 +1,213 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Included after actual generated parsers and the shared host doubles.
* Signature bytes have valid SSH framing; crypto is intentionally doubled. */
static void check_ecc_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
word32 n, byte *end, int good)
{
byte storage[512], before[512], digest[32]={0};
assert(n+32<=sizeof(storage));
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx};
/* Accessible canaries catch writes; a second run ends at a guard page. */
for (int guarded=0; guarded<2; guarded++) {
memset(storage,0xa5,sizeof(storage));
byte *p=guarded?end-n:storage+16;
memcpy(p,frame,n);
memcpy(before,storage,sizeof(storage));
pk.signature=p; pk.signatureSz=n;
/* Instrument actual length reads as well as crypto calls: accessible
* bytes after a short sub-blob must not even supply the s header. */
word32 inner=8+pk.publicKeyTypeSz, declared=0;
if (n>=inner) {
ato32(frame+inner-4,&declared);
if (declared<=n-inner) {
nested_begin=(uintptr_t)(p+inner);
nested_end=nested_begin+declared;
nested_outer_end=(uintptr_t)(p+n);
}
}
imports=converts=verifies=0;
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
nested_begin=nested_end=nested_outer_end=0;
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
assert(imports==1 && converts==(unsigned)good && verifies==(unsigned)good);
assert(memcmp(storage,before,sizeof(storage))==0);
assert(memcmp(p,frame,n)==0);
cases++;
}
}
static void ecc_framing(byte *end)
{
const byte type[]="ecdsa-sha2-nistp256";
byte key[128]={0}, frame[256], r[33], s[33];
word32 k=string(key,type,sizeof(type)-1);
k+=string(key+k,(const byte*)"nistp256",8);
k+=string(key+k,(const byte*)"Q",1);
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeySz=k,
.publicKeyType=type,.publicKeyTypeSz=sizeof(type)-1};
/* Both ordinary positive mpints and leading-zero sign padding. */
for (word32 rn=32;rn<=33;rn++) for (word32 sn=32;sn<=33;sn++) {
memset(r,0x61,sizeof(r)); memset(s,0x62,sizeof(s));
if (rn==33) { r[0]=0; r[1]=0x80; }
if (sn==33) { s[0]=0; s[1]=0x80; }
memset(frame,0x5a,sizeof(frame));
word32 length_at=string(frame,type,sizeof(type)-1);
word32 inner=length_at+4, blob=8+rn+sn;
put(frame+length_at,blob);
word32 r_at=inner, s_at=inner+4+rn;
string(frame+r_at,r,rn); string(frame+s_at,s,sn);
word32 total=inner+blob;
expected_r=r; expected_s=s; expected_r_sz=rn; expected_s_sz=sn;
check_ecc_frame(pk,frame,total,end,1);
/* All cuts within the nested blob. Backing bytes still contain a
* complete r/s pair: old code consumed beyond the declared boundary. */
for (word32 declared=0;declared<=blob+2;declared++) {
put(frame+length_at,declared);
check_ecc_frame(pk,frame,total,end,declared==blob);
}
const word32 huge[]={UINT32_MAX,UINT32_MAX-inner+1,0x80000000};
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
put(frame+length_at,huge[j]);
check_ecc_frame(pk,frame,total,end,0);
}
put(frame+length_at,blob);
/* Every physical/logical truncation after the valid label, including
* partial nested length headers and both mpints. */
for (word32 cut=length_at;cut<total;cut++)
check_ecc_frame(pk,frame,cut,end,0);
/* A complete pair plus trailing bytes, inside or outside the declared
* sub-blob, must not reach conversion/verification. */
for (word32 extra=1;extra<=8;extra++) {
put(frame+length_at,blob);
check_ecc_frame(pk,frame,total+extra,end,0);
put(frame+length_at,blob+extra);
check_ecc_frame(pk,frame,total+extra,end,0);
}
put(frame+length_at,blob);
for (unsigned which=0;which<2;which++) {
word32 at=which?s_at:r_at, real=which?sn:rn;
word32 wrong[]={0,1,real-1,real+1,blob,UINT32_MAX,0xfffffffc};
for (unsigned j=0;j<sizeof(wrong)/sizeof(*wrong);j++) {
put(frame+at,wrong[j]);
check_ecc_frame(pk,frame,total,end,0);
}
put(frame+at,real);
}
/* A correctly framed signature still propagates crypto rejection. */
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
pk.signature=frame; pk.signatureSz=total;
verify_failure=1; converts=verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,32)==WS_ECC_E);
assert(converts==1 && verifies==1); verify_failure=0; cases++;
}
expected_r=expected_s=NULL;
}
static void reset_ed(void)
{ ed_imports=ed_starts=ed_updates=ed_finals=0; }
static void check_ed_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
word32 n, byte *end, int good)
{
byte storage[512], before[512], data[256], session[32];
memset(data,0x7c,sizeof(data)); memset(session,0x23,sizeof(session));
assert(n+32<=sizeof(storage));
struct context ctx={0};
WOLFSSH ssh={.ctx=&ctx,.sessionId=session,.sessionIdSz=sizeof(session)};
WS_UserAuthData auth={.usernameSz=4,.serviceNameSz=14,.authNameSz=9};
pk.dataToSign=data;
word32 signedSz=4+14+9+1+pk.publicKeyTypeSz+pk.publicKeySz+20;
assert(signedSz<=sizeof(data));
for (int guarded=0;guarded<2;guarded++) {
memset(storage,0xa5,sizeof(storage));
byte *p=guarded?end-n:storage+16;
memcpy(p,frame,n); memcpy(before,storage,sizeof(storage));
pk.signature=p; pk.signatureSz=n;
reset_ed();
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
assert(ed_imports==1 && ed_starts==(unsigned)good);
assert(ed_updates==(good?4U:0U) && ed_finals==(unsigned)good);
if (good) {
byte expected[512];
put(expected,sizeof(session)); memcpy(expected+4,session,sizeof(session));
expected[4+sizeof(session)]=MSGID_USERAUTH_REQUEST;
memcpy(expected+5+sizeof(session),data,signedSz);
assert(ed_message_sz==5+sizeof(session)+signedSz);
assert(memcmp(ed_message,expected,ed_message_sz)==0);
}
assert(memcmp(storage,before,sizeof(storage))==0);
assert(memcmp(p,frame,n)==0);
cases++;
}
}
static void ed25519_framing(byte *end)
{
const byte type[]="ssh-ed25519";
const word32 typeSz=sizeof(type)-1;
byte key[128], frame[256], rawkey[32], rawsig[64], data[256]={0};
memset(rawkey,0x45,sizeof(rawkey)); memset(rawsig,0x76,sizeof(rawsig));
expected_ed_sig=rawsig; expected_ed_sig_sz=sizeof(rawsig);
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; WS_UserAuthData auth={0};
/* Both label checks, expected type ends at protected memory. */
byte *expected=end-typeSz; memcpy(expected,type,typeSz);
for (unsigned which=0;which<2;which++) for(unsigned mode=0;mode<6;mode++) {
byte bad[40]={0}; memcpy(bad,type,typeSz); word32 n=typeSz;
if(mode==1) bad[0]='X';
if(mode==2) n--;
if(mode==3) n++;
if(mode==4) n=0;
if(mode==5) n=sizeof(bad);
word32 k=string(key,which==0?bad:type,which==0?n:typeSz);
k+=string(key+k,rawkey,sizeof(rawkey));
word32 f=string(frame,which==1?bad:type,which==1?n:typeSz);
f+=string(frame+f,rawsig,sizeof(rawsig));
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=expected,
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
.signatureSz=f,.dataToSign=data};
reset_ed();
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
if(mode==0) {
assert(ret==0 && ed_imports==1 && ed_starts==1 && ed_updates==4 && ed_finals==1);
}
else {
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
assert(ed_imports==which && ed_starts==0 && ed_updates==0 && ed_finals==0);
}
cases++;
}
word32 k=string(key,type,typeSz); k+=string(key+k,rawkey,sizeof(rawkey));
memset(frame,0x5a,sizeof(frame));
word32 length_at=string(frame,type,typeSz);
word32 total=length_at+string(frame+length_at,rawsig,sizeof(rawsig));
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=type,
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
.signatureSz=total,.dataToSign=data};
for (word32 declared=0;declared<=66;declared++) {
put(frame+length_at,declared);
check_ed_frame(pk,frame,total,end,declared==64);
}
const word32 huge[]={UINT32_MAX,UINT32_MAX-length_at,0x80000000};
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
put(frame+length_at,huge[j]); check_ed_frame(pk,frame,total,end,0);
}
put(frame+length_at,64);
for (word32 cut=0;cut<total;cut++)
check_ed_frame(pk,frame,cut,end,0);
for (word32 extra=1;extra<=8;extra++)
check_ed_frame(pk,frame,total+extra,end,0);
/* Key truncations must not import or start signature verification. */
for (word32 cut=0;cut<k;cut++) {
byte *p=end-cut; memcpy(p,key,cut);
pk.publicKey=p; pk.publicKeySz=cut; reset_ed();
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_CRYPTO_FAILED);
assert(ed_imports==0 && ed_starts==0 && ed_finals==0); cases++;
}
pk.publicKey=key; pk.publicKeySz=k; reset_ed(); verify_failure=1;
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_ED25519_E);
assert(ed_starts==1 && ed_updates==4 && ed_finals==1);
verify_failure=0; expected_ed_sig=NULL; cases++;
}
+256
View File
@@ -0,0 +1,256 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Extracted wolfSSH functions retain upstream GPL notices in generated source.
* Crypto doubles test parser gating, not cryptographic validity. */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t byte;
typedef uint32_t word32;
#define WS_SUCCESS 0
#define WS_BUFFER_E -1
#define WS_BAD_ARGUMENT -2
#define WS_INVALID_CHANID -3
#define WS_OVERFLOW_E -4
#define WS_MEMORY_E -5
#define WS_INVALID_ALGO_ID -6
#define WS_CRYPTO_FAILED -7
#define WS_ECC_E -8
#define WS_ED25519_E -9
#define MSGID_USERAUTH_REQUEST 50
#define MSG_ID_SZ 1
#define BOOLEAN_SZ 1
#define Ed25519 0
#define UINT32_SZ 4
#define WOLFSSH_MAX_NAMESZ 32
#define CLIENT_USERAUTH_REQUEST_DONE 42
#define WS_CHANNEL_ID_SELF 0
#define WLOG(...) ((void)0)
#define WOLFSSH_UNUSED(x) ((void)(x))
#define WMEMCPY memcpy
#define WMEMCMP memcmp
#define WMALLOC(n,h,t) malloc(n)
#define WFREE(p,h,t) free(p)
#define ECDSA_ASN_SIG_SZ 80
#define INVALID_DEVID -1
#define WC_SIGNATURE_TYPE_ECC 1
struct context { void *heap; };
typedef struct {
struct context *ctx;
int clientState;
const byte *sessionId;
word32 sessionIdSz;
} WOLFSSH;
typedef struct { word32 peerWindowSz; } WOLFSSH_CHANNEL;
typedef struct {
const byte *publicKey, *publicKeyType, *signature;
word32 publicKeySz, publicKeyTypeSz, signatureSz;
const byte *dataToSign;
} WS_UserAuthData_PublicKey;
typedef struct {
word32 usernameSz, serviceNameSz, authNameSz;
} WS_UserAuthData;
typedef struct { int unused; } ed25519_key;
typedef struct { int unused; } ecc_key;
enum wc_HashType { HASH_SHA256 };
static unsigned imports, converts, verifies, finds, cases;
static unsigned ed_imports, ed_starts, ed_updates, ed_finals;
static const byte *expected_r, *expected_s, *expected_ed_sig;
static word32 expected_r_sz, expected_s_sz, expected_ed_sig_sz;
static byte ed_message[512];
static word32 ed_message_sz;
static int verify_failure;
static uintptr_t nested_begin, nested_end, nested_outer_end;
static WOLFSSH_CHANNEL channel;
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *ssh, word32 id, int side)
{ finds++; return id == 7 ? &channel : NULL; }
static void ato32(const byte *p, word32 *v)
{
uintptr_t address=(uintptr_t)p;
if (nested_begin != 0 && address>=nested_begin && address<nested_outer_end)
assert(address<=nested_end && 4<=nested_end-address);
*v = ((word32)p[0]<<24) | ((word32)p[1]<<16) | ((word32)p[2]<<8) | p[3];
}
static void put(byte *p, word32 v)
{ p[0]=v>>24; p[1]=v>>16; p[2]=v>>8; p[3]=v; }
static int wc_ecc_init_ex(ecc_key *k, void *h, int id) { return 0; }
static int wc_ecc_import_x963(const byte *p, word32 n, ecc_key *k)
{ imports++; return 0; }
static void wc_ecc_free(ecc_key *k) {}
static int wc_ecc_rs_raw_to_sig(const byte *r, word32 rn, const byte *s,
word32 sn, byte *out, word32 *n)
{
converts++;
if (expected_r != NULL) {
assert(rn==expected_r_sz && sn==expected_s_sz);
assert(memcmp(r,expected_r,rn)==0 && memcmp(s,expected_s,sn)==0);
}
return 0;
}
static int wc_SignatureVerifyHash(enum wc_HashType h, int t, byte *d,
word32 dn, byte *s, word32 sn, ecc_key *k, size_t kn)
{ verifies++; return verify_failure; }
static void c32toa(word32 v, byte *p) { put(p,v); }
static int wc_ed25519_init_ex(ed25519_key *key, void *heap, int id) { return 0; }
static void wc_ed25519_free(ed25519_key *key) {}
static int wc_ed25519_import_public(const byte *p, word32 n, ed25519_key *key)
{ ed_imports++; assert(n==32); return 0; }
static int wc_ed25519_verify_msg_init(const byte *sig, word32 n,
ed25519_key *key, byte type, const byte *context, byte contextSz)
{
ed_starts++;
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
ed_message_sz=0;
return 0;
}
static int wc_ed25519_verify_msg_update(const byte *p, word32 n, ed25519_key *key)
{
ed_updates++;
assert(n<=sizeof(ed_message)-ed_message_sz);
if (n != 0)
memcpy(ed_message+ed_message_sz,p,n);
ed_message_sz+=n;
return 0;
}
static int wc_ed25519_verify_msg_final(const byte *sig, word32 n,
int *status, ed25519_key *key)
{
ed_finals++;
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
*status=!verify_failure;
return 0;
}
#include "actual.c"
static void parsers(byte *end)
{
struct context ctx = {0}; WOLFSSH ssh = {.ctx=&ctx, .clientState=9};
const word32 lengths[] = {0,1,2,3,4,27,28,31,32,33,255,UINT32_MAX-4,UINT32_MAX};
for (word32 n=0; n<=64; n++) {
byte *p=end-n;
for (unsigned j=0; j<sizeof(lengths)/sizeof(*lengths); j++) {
memset(p, 'x', n);
if (n>=4) put(p,lengths[j]);
word32 idx=0, value=123;
int good=n>=4 && lengths[j]<=n-4;
assert(GetSize(&value,p,n,&idx)==(good?0:WS_BUFFER_E));
idx=0;
assert(DoIgnore(&ssh,p,n,&idx)==(good?0:WS_BUFFER_E));
if (good) assert(idx==4+lengths[j]);
idx=0; ssh.clientState=9;
int service=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==(service?0:WS_BUFFER_E));
assert(ssh.clientState==(service?42:9));
assert(idx==(service?4+lengths[j]:0));
char out[10]; memset(out, 0x55, sizeof(out));
word32 cap=8; idx=0;
assert(GetString(out+1,&cap,p,n,&idx)==(good?0:WS_BUFFER_E));
assert(out[0]==0x55 && out[9]==0x55);
if(good) { assert(cap==(lengths[j]<8?lengths[j]:7)); assert(out[cap+1]==0); }
cap=0; idx=0;
assert(GetString((char*)end,&cap,p,n,&idx)==WS_BUFFER_E);
assert(idx==0);
cases++;
}
word32 invalids[]={n,n+1,UINT32_MAX-3,UINT32_MAX};
for(unsigned j=0;j<4;j++) {
word32 idx=invalids[j], v=0;
assert(GetSize(&v,p,n,&idx)==WS_BUFFER_E);
idx=invalids[j]; assert(DoIgnore(&ssh,p,n,&idx)==WS_BUFFER_E);
idx=invalids[j]; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,n,&idx)==WS_BUFFER_E);
assert(ssh.clientState==9 && idx==invalids[j]); cases++;
}
}
/* Nonzero packet offsets and exact-end empty strings. */
byte p[12]={0}; put(p+3,5); word32 idx=3;
assert(DoIgnore(&ssh,p,12,&idx)==0 && idx==12);
put(p+3,0); idx=3;
assert(DoServiceRequest(&ssh,p,7,&idx)==0 && idx==7);
}
static void windows(byte *end)
{
WOLFSSH ssh={0};
for(word32 n=0;n<8;n++) {
byte *p=end-n; memset(p,0,n); word32 idx=0;
channel.peerWindowSz=123; finds=0;
assert(DoChannelWindowAdjust(&ssh,p,n,&idx)==WS_BUFFER_E);
assert(channel.peerWindowSz==123 && finds==0 && idx==0); cases++;
}
word32 values[]={0,1,2,0x7fffffff,0xfffffffe,UINT32_MAX};
for(unsigned a=0;a<6;a++) for(unsigned b=0;b<6;b++) {
byte p[11]={0}; put(p+3,7); put(p+7,values[b]); word32 idx=3;
channel.peerWindowSz=values[a];
int overflow=values[b]>UINT32_MAX-values[a];
assert(DoChannelWindowAdjust(&ssh,p,11,&idx)==(overflow?WS_OVERFLOW_E:0));
assert(channel.peerWindowSz==(overflow?values[a]:values[a]+values[b]));
assert(idx==11); cases++;
}
byte p[8]={0}; word32 idx=0; channel.peerWindowSz=12;
assert(DoChannelWindowAdjust(&ssh,p,8,&idx)==WS_INVALID_CHANID);
assert(channel.peerWindowSz==12);
}
static word32 string(byte *p, const byte *s, word32 n)
{ put(p,n); memcpy(p+4,s,n); return n+4; }
static void ecc(byte *end)
{
const byte type[]="ecdsa-sha2-nistp256";
const word32 size=sizeof(type)-1;
byte *expected=end-size; memcpy(expected,type,size);
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
for(int which=0;which<2;which++) for(int mode=0;mode<6;mode++) {
byte key[128]={0},sig[128]={0}, bad[40]={0};
memcpy(bad,type,size); word32 n=size;
if(mode==1) bad[0]='X'; /* equal length mismatch */
if(mode==2) n--; /* matching prefix, shorter */
if(mode==3) n++; /* expected ends at guard page */
if(mode==4) n=0;
if(mode==5) n=sizeof(bad);
word32 k=string(key,which==0?bad:type,which==0?n:size);
k+=string(key+k,(const byte*)"nistp256",8);
k+=string(key+k,(const byte*)"Q",1);
word32 s=string(sig,which==1?bad:type,which==1?n:size);
put(sig+s,10); s+=4;
s+=string(sig+s,(const byte*)"r",1);
s+=string(sig+s,(const byte*)"s",1);
WS_UserAuthData_PublicKey pk={.publicKey=key, .publicKeyType=expected,
.signature=sig, .publicKeySz=k, .publicKeyTypeSz=size, .signatureSz=s};
imports=converts=verifies=0;
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
if(mode==0) assert(ret==0 && imports==1 && converts==1 && verifies==1);
else {
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
assert(imports==(unsigned)which && converts==0 && verifies==0);
}
cases++;
if (mode==0 && which==0) {
for (word32 cut=0; cut<k; cut++) {
pk.publicKeySz=cut; verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
assert(verifies==0); cases++;
}
pk.publicKeySz=k;
for (word32 cut=0; cut<s; cut++) {
pk.signatureSz=cut; verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
assert(verifies==0); cases++;
}
}
}
}
#include "auth_framing.c"
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *map=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(map!=MAP_FAILED && mprotect(map+page,page,PROT_NONE)==0);
parsers(map+page); windows(map+page); ecc(map+page);
ecc_framing(map+page); ed25519_framing(map+page);
assert(munmap(map,(size_t)page*2)==0);
printf("PASS: %u parser/window/ECC/Ed25519 cases, guard pages + UBSan trap\n",cases);
return 0;
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Host contracts from freshly generated, original-hash-verified wolfSSH source."""
import os
import resource
from pathlib import Path
import subprocess
import sys
import tempfile
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'tests/wolfssh_auth_contract'))
from run import extract
from security_overrides import ENTRIES, render_entry
entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
assert entry.sha256 == '81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
original, generated = render_entry(entry, {'project': ROOT})
names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef',
'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoUserAuthRequestEd25519')
# Verify this slice cannot accidentally change ordering or existing password logic.
from security_overrides import apply_edits, MODIFICATION_NOTICE, WOLFSSH_PARSER_EDITS
baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), entry.edits[len(WOLFSSH_PARSER_EDITS):])
for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure',
'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'):
assert extract(generated.decode(), name) == extract(baseline, name), name
with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
work = Path(directory)
# Read back the actual generated bytes, not a parallel implementation.
source = work / 'internal.c'
source.write_bytes(generated)
functions = '\n'.join(extract(source.read_text(), n) for n in names)
(work / 'actual.c').write_text(functions)
for small in (False, True):
binary = work / ('contract-small' if small else 'contract')
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-fsanitize=undefined',
'-fsanitize-undefined-trap-on-error',
*(['-DWOLFSSH_SMALL_STACK'] if small else []),
'-I', str(work), str(HERE / 'contract.c'), '-o', str(binary)],
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
subprocess.run([str(binary)], check=True, timeout=30)
# Prove negative fixtures detect removal of each new boundary/type guard.
# Mutations affect only temporary extracted host copies, never the override.
mutations = (
('ECC nested read boundary', 'DoUserAuthRequestEcc',
(('pk->signature, sz, &i)', 'pk->signature, pk->signatureSz, &i)'),)),
('ECC inner exact consumption', 'DoUserAuthRequestEcc',
(('(i != sz || sz != pk->signatureSz)', '(sz != pk->signatureSz)'),)),
('ECC outer exact consumption', 'DoUserAuthRequestEcc',
(('(i != sz || sz != pk->signatureSz)', '(i != sz)'),)),
('Ed25519 key label', 'DoUserAuthRequestEd25519',
(('|| WMEMCMP(publicKeyType,', '&& WMEMCMP(publicKeyType,'),)),
('Ed25519 signature label', 'DoUserAuthRequestEd25519',
(('pk->publicKeyTypeSz ||', 'pk->publicKeyTypeSz &&'),)),
('Ed25519 outer exact consumption', 'DoUserAuthRequestEd25519',
(('if (ret == WS_SUCCESS && sz != pk->signatureSz - i)', 'if (0)'),)),
)
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
for label, name, replacements in mutations:
body = extract(source.read_text(), name)
changed = body
for old, new in replacements:
assert old in changed, label
changed = changed.replace(old, new)
assert changed != body, label
(work / 'actual.c').write_text(functions.replace(body, changed))
binary = work / 'mutation'
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-I', str(work),
str(HERE / 'contract.c'), '-o', str(binary)],
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected mutation: {label}'
print(f'PASS: {len(mutations)} parser guard-removal mutations rejected')
print('PASS: exact original hash; generated parser; unchanged ordering/password/deferred functions')
print('NOTE: production build-tree registration/firmware not regenerated or validated')