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;
}