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.
16 KiB
SSH key-validation review — 2026-09-15
Decision and scope
The baseline P-256 ECDH and X25519 validation gaps are mitigated by project-owned compile policy. Parent firmware build and local strict crypto suite PASS; the broader SSH review and hardware gates remain open. This supplements, rather than silently rewrites, the historical wolf review.
- Enable upstream
WOLFSSL_VALIDATE_ECC_IMPORTfor wolfSSL 5.8.2. The SSH server imports an unauthenticated P-256 peer point and otherwise reaches scalar multiplication without an on-curve check. - Enable existing upstream
WOLFSSL_ECDHX_SHARED_NOT_ZERO. The SSH X25519 input precheck does not reject every low-order input; the result check was disabled. - These are PUBLIC definitions in
cmake/wolf_crypto_policy.cmake, with resolved-settings checks incmake/wolf_crypto_policy.h. No rootCMakeLists.txtedit is necessary: it already includes this module afterproject(). The existing small X25519/Ed25519 policy and RNG/acceleration controls are preserved. - No dependency version, installed vendor source, generated override, application source, or device change was made by this task. Edits are restricted to the assigned policy files,
tests/wolf_crypto_policy/, and this report. - This is not a demonstrated long-term-key recovery, authentication bypass, remotely measured exploit, full upstream backport, or release approval. ECDH uses a freshly generated ephemeral key, separate from the long-term signing identity.
Exact local evidence
Inspected installed wolfSSH 1.4.20 / wolfSSL 5.8.2~1 and the generated wolfSSH input located through .pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json. SHA-256 snapshot:
| Input | SHA-256 |
|---|---|
managed_components/wolfssl__wolfssh/src/internal.c |
81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9 |
.pio/build/esp32-s3-devkitc-1-n16r8/security_overrides/wolfssh_internal/internal.c |
9e5923d536cee049409a7df471d155a6dd2e804d55a1528f5c4abc8e34807dea |
managed_components/wolfssl__wolfssl/wolfcrypt/src/ecc.c |
909c57e2756a8002df9f1d214483c659cb20db4a5f51047eda62d64ac458db06 |
managed_components/wolfssl__wolfssl/wolfcrypt/src/curve25519.c |
9a0f6f0205245a8d19500a936d9b02bb71c8656713648408d1cb408362694b76 |
managed_components/wolfssl__wolfssl/wolfcrypt/src/signature.c |
62ab3db3dfd251b2a2c73b69ef05aab6085d2e0d673fd9159514b3ee261cea4f |
The original and generated bodies of HashForId, KeyAgreeEcdh_server, KeyAgreeCurve25519_server, SignHEcdsa, and DoUserAuthRequestPublicKey remain byte-identical. DoUserAuthRequestEcc and DoUserAuthRequestEd25519 now contain the reviewed label/framing corrections from tools/security_overrides.py. The crypto suite independently reconstructs only those exact deltas from the hash-pinned original, requires exact anchor counts, and compares all seven complete functions with the actual generated compilation input. It does not derive its expected bodies from the generator being checked, skip changed functions, normalize away changes or relax the original hashes. Any additional change requires re-review. This is not a whole-generator equivalence test.
Generated-source line references below retain the initial pre-parser-correction snapshot; use the named functions in the current generated source, whose hash is recorded above.
Before changing policy, actual Xtensa preprocessing of ecc.c confirmed USE_FAST_MATH, ECC_TIMING_RESISTANT, WOLFSSL_SMALL_STACK, HAVE_ECC_CHECK_KEY, and internal HAVE_ECC_CHECK_PUBKEY_ORDER; neither validation definition was present. USE_ECC_B_PARAM and SP math were absent. Initial candidate replay checked the proposed definitions. Following the parent build, strict mode now passes without injecting policy flags, checking the saved production compiler/includes/definitions and syntax for twelve library/SSH/application translation units, including ecc.c and signature.c. Parent-reported pio run PASS: 94,340 B linked RAM / 1,768,949 B flash. This agent did not rerun PlatformIO or inspect a flashed image; compile-profile checks and the supplied link/build result are distinct evidence.
P-256 import and ECDH
Generated internal.c:11251–11317, KeyAgreeEcdh_server:
- Derive
primeIdfrom negotiated KEX; project policy allowsecdh-sha2-nistp256and X25519. - Initialize public/private keys; attach the session RNG to the ephemeral private key.
wc_ecc_import_x963_ex(handshake->e, eSz, pubKey, primeId)imports the peer point.- Only after successful import, generate a fresh private key, export its public point, and call
wc_ecc_shared_secret. - Errors propagate; both key objects are freed. The policy adds no task, queue, retry loop, protocol control sequence, or new entropy source.
Installed ecc.c:10709–11020 parses X9.63 coordinates and selects the curve. At 10993–10996, wc_ecc_check_key is called only with WOLFSSL_VALIDATE_ECC_IMPORT. Merely compiling HAVE_ECC_CHECK_KEY is not equivalent. The inferred-curve wc_ecc_import_x963 wrapper uses the same implementation.
Without this flag, wc_ecc_shared_secret (4669–4752) checks pointers, private-key type, domain metadata and matching curve IDs, not whether the peer coordinates satisfy the curve equation. Its software path reaches wc_ecc_shared_secret_ex (5102), wc_ecc_shared_secret_gen_sync (4759), and wc_ecc_mulmod_ex2 (4942 vicinity), using curve A, prime and order. No equivalent point-validation call precedes the multiplication. The absent import validation is applicable, not just an unproven macro concern.
With the flag, _ecc_validate_public_key (10495–10703) checks infinity, coordinate ranges, the curve equation and public-point order; private imports additionally check the private range and private/public consistency where applicable. It loads B from key->dp->Bf even when USE_ECC_B_PARAM is absent. There is no need to force B storage or reproduce the larger upstream source refactor. The guard rejects configurations that disable the software validator or route it to the known successful hardware stubs; it does not claim arbitrary future backends are validated.
The work is bounded by the selected curve and existing key/input limits, but not free: valid imports incur public validation, including order checking, and private/public consistency checks can add multiplication and allocations. Target latency, memory peaks, stack margins, repeated-handshake/rekey load and the existing deadlines must be measured. Existing admission limiting is not evidence that this cost is harmless.
X25519 all-zero result
Generated internal.c:11335–11394, KeyAgreeCurve25519_server, explicitly runs wc_curve25519_check_public before import, key generation and shared-secret calculation. Installed curve25519.c:645–710 rejects wrong lengths, zero/one, the high bit, and the upper-end range in the little-endian path. This is not a complete low-order rejection rule.
The real vendor small-math tests exercise two nontrivial low-order u-coordinates that pass that precheck:
e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b8005f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157
wc_curve25519_shared_secret_ex (452–536) already contains a 32-byte OR reduction under WOLFSSL_ECDHX_SHARED_NOT_ZERO. It returns ECC_OUT_OF_RANGE_E for an all-zero result before copying it to the caller, then wipes its temporary. Neither the current SSH server helper nor subsequent successful-KEX processing adds an equivalent result test. Enabling this existing check is sufficient for the inspected software path. Tests verify rejection of both inputs, unchanged caller output on rejection, ordinary zero/one precheck rejection, and the valid RFC7748 shared secret. No custom blacklist or small-math/blinding combination is introduced.
Raw signatures and CVE-2026-5194 applicability
No attacker-selected short digest/OID-confusion trigger was found in the reviewed current raw SSH authentication/signing paths. The separate ECC/Ed25519 label and signature-framing defects have now been corrected by the parser owner, as reviewed below; that does not backport generic crypto API hardening.
src/ssh_transport.c:489callsuser_database_authorize_ssh_public_keybefore wolfSSH signature verification.src/user_database.c:185vicinity checks exact embedded type, exactnistp256, 65-byte uncompressed point and end-of-blob; mbedTLS parses and checks the point.user_database_authorize_ssh_public_key:810repeats validation and requires exact stored key-type/blob matching for the named account. Thus the server user-key path already has an independent P-256 point-validation boundary, unlike unauthenticated KEX. The new wolfSSL import check is defense in depth here.- Generated
DoUserAuthRequestPublicKey:7324vicinity deriveshashId = HashForId(pkTypeId), obtainsdigestSzfromwc_HashGetDigestSize, and hashes the session ID and authentication message locally.HashForIdmaps P256 to SHA256: the digest is 32 bytes, not a peer-supplied digest length. The untrusted signature type does not choose an alternate prehash independently of that authorized key type. - Generated
DoUserAuthRequestEcc:6851vicinity imports Q, converts raw r/s throughwc_ecc_rs_raw_to_sig, and callswc_SignatureVerifyHashwith that locally computed digest and length. Installedsignature.c:131only checks that the hash type exists, not equality ofhash_lenwith its size;ecc.c:9204checks r/s ranges but predates the new minimum-digest check. These upstream API weaknesses remain in the dependency, but the reviewed caller supplies the correct size. SignHEcdsa:11676hashes exchange hash H usingHashForId(handshake->pubKeyId)and signs the resulting full SHA256 digest for the allowed P256 host key. This is distinct from ECDH.- Client host verification (
DoKexDhReply, generated calls near5651/5685) useswc_SignatureVerify, which computes the full digest before verification. ClientBuildUserAuthRequestEccderives its digest size fromHashForId(keySigId). These are not the intended application's server authentication role. Existing message-order concerns mean role alone must not substitute for validating dispatch reachability. - Certificate variants (
DoUserAuthRequestEccCert,BuildUserAuthRequestEccCert) are under disabledWOLFSSH_CERTS. Current Ed25519 auth takes its separate message/streaming-verification path, not an attacker-sized Ed25519ph digest. Ed448 and ML-DSA are not current SSH algorithms. - Host private DER decoding goes through
wc_EccPrivateKeyDecodein installedasn.c:35833; template parsing callswc_ecc_import_private_key_exnear36033. Real host tests cover valid SEC1 P256 private/public import without a pre-attached RNG, plus rejection of a corrupted embedded public point. This is not an NVS lifecycle or identity-rotation test.
The compile-policy mitigation does not backport PR10131's global digest-length/OID enforcement. Reassess if new raw APIs, certificates, key types, callbacks or client roles are enabled.
Parser-owner corrections reviewed and remaining work
The following formerly pending gaps are fixed in the current generated input, not by the crypto compile flags:
- Both key/signature label checks in
DoUserAuthRequestEccandDoUserAuthRequestEd25519now use OR. Unequal lengths reject beforememcmp; equal lengths compare the bounded expected span. This rejects equal-length wrong labels and avoids comparing an oversized label against the shorter expected label. Existing error normalization remains unchanged. - ECC
GetSizefirst provessz <= signatureSz - i; convertingszto the absolute end withsz += itherefore cannot wrap. BothGetStringRefcalls use that end, not the outer field size. The subsequenti != sz || sz != pk->signatureSzrejection requires exact inner and outer consumption before conversion or verification. - Ed25519 requires
sz == pk->signatureSz - ibefore starting streaming signature verification. Trailing bytes outside the declared signature string now reject.
Reviewed tools/security_overrides.py and tests/wolfssh_parser_contract/{run.py,README.md,auth_framing.c} against exact original/generated function diffs. These deltas leave ECC digest creation, raw-to-DER conversion and crypto calls, and Ed25519 streamed-message construction unchanged. Valid framing is retained; previously tolerated malformed labels/trailing bytes reject. No exploit or authentication-bypass demonstration is claimed.
The parser suite uses extracted generated functions, guard pages, instrumented nested reads, UBSan trap mode and crypto doubles. Its documented 3,124 cases per stack mode and six guard-removal mutations concern parser gating, not actual signature arithmetic. In contrast, this crypto suite executes real installed vendor arithmetic/ASN/wrappers and separately checks exact generated parser deltas and production compilation settings. Neither suite is an end-to-end SSH handshake test.
Parent build plus local strict crypto validation resolve the earlier build/profile handoff; no root edit is requested. Remaining work: broader ordering/state-machine and deferred parser/advisory review, standalone ECC curve-name/key-blob semantic validation if that dependency path is used without the application's existing checks, generic PR10131 API hardening as applicability requires, and whole-phase hardware/resource/latency tests. Later changes to audited functions still require explicit delta review, not silent repinning.
Tests and limitations
Commands run for this task:
python3 tests/wolf_crypto_policy/run.py --host-only
python3 tests/wolf_crypto_policy/run.py --candidate
# Follow-up after parent firmware build:
python3 tests/wolf_crypto_policy/run.py
Follow-up strict suite PASS, including all host vectors and private ASN-decode cases, seven exact source-body checks with independently specified parser deltas, and production flags without candidate injection. Earlier host-only/candidate runs also passed. Parent build evidence is supplied, not rerun here; no device validation was performed. Details are in the companion test README. Coverage includes 20 guard cases; a host CMake fixture executing the real module over a library → SSH → app graph; real installed small-X25519/Ed25519 and TFM P256 crypto; explicit/inferred import rejection for off-curve, infinity, out-of-range, truncated and wrong-tag points; valid ECDH; raw/DER valid and invalid ECDSA verification; private-key ASN import; exact source checks; twelve strict production target macro/syntax checks; four real-settings missing-policy rejection cases.
Host settings use software TFM, ECC timing resistance and small-stack allocation, but host word size, allocator, OS entropy, compiler and absent ESP acceleration differ from firmware. The CMake fixture is not the full ESP-IDF graph. No exhaustive Wycheproof/fuzz campaign, allocator-failure injection, crypto-suite sanitizer execution, network handshake, real rekey, timing/side-channel measurement, stack/heap reserve measurement, agent-performed firmware link, flashing or hardware validation is claimed. The parent-reported firmware build/link and size figures above do not establish runtime reserves. Test development exposed host fixture omissions (POSIX declarations, wolfmath linkage, filesystem RNG and ASN settings); those were corrected without modifying vendor sources.
External sources rechecked
Read-only retrieval on 2026-09-15:
- wolfSSL PR10133 diff: removes conditional B/on-curve gating and treats
wc_ecc_import_x963_exinput as untrusted by default in the later tree. This is not a directly applied patch to 5.8.2. - 5.9.1 tagged ChangeLog, Bug Fixes: explicitly recommends
WOLFSSL_VALIDATE_ECC_IMPORTfor users of older versions. This is the basis for the bounded policy choice. - wolfSSL PR10374 diff: makes X25519/X448 all-zero checking opt-out. The existing 5.8.2 opt-in macro enables the inspected equivalent synchronous result check; later nonblocking/TLS changes are not imported.
- wolfSSL PR10131 diff: certificate signature-OID/key-type consistency plus raw digest-size hardening; used to distinguish the current SSH caller contract from unpatched generic API behavior.
PR URLs are mutable and are not an archived commit-pinned upstream evidence bundle. Local original source hashes above and the source-contract tests bound the implementation inspected here.