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