# 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](wolf_security_review.md). - Enable upstream `WOLFSSL_VALIDATE_ECC_IMPORT` for 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 in `cmake/wolf_crypto_policy.h`. No root `CMakeLists.txt` edit is necessary: it already includes this module after `project()`. 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`: 1. Derive `primeId` from negotiated KEX; project policy allows `ecdh-sha2-nistp256` and X25519. 2. Initialize public/private keys; attach the session RNG to the ephemeral private key. 3. `wc_ecc_import_x963_ex(handshake->e, eSz, pubKey, primeId)` imports the peer point. 4. Only after successful import, generate a fresh private key, export its public point, and call `wc_ecc_shared_secret`. 5. 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: - `e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800` - `5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157` `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:489` calls `user_database_authorize_ssh_public_key` before wolfSSH signature verification. `src/user_database.c:185` vicinity checks exact embedded type, exact `nistp256`, 65-byte uncompressed point and end-of-blob; mbedTLS parses and checks the point. `user_database_authorize_ssh_public_key:810` repeats 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:7324` vicinity derives `hashId = HashForId(pkTypeId)`, obtains `digestSz` from `wc_HashGetDigestSize`, and hashes the session ID and authentication message locally. `HashForId` maps 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:6851` vicinity imports Q, converts raw r/s through `wc_ecc_rs_raw_to_sig`, and calls `wc_SignatureVerifyHash` with that locally computed digest and length. Installed `signature.c:131` only checks that the hash type exists, not equality of `hash_len` with its size; `ecc.c:9204` checks 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:11676` hashes exchange hash H using `HashForId(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 near `5651/5685`) uses `wc_SignatureVerify`, which computes the full digest before verification. Client `BuildUserAuthRequestEcc` derives its digest size from `HashForId(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 disabled `WOLFSSH_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_EccPrivateKeyDecode` in installed `asn.c:35833`; template parsing calls `wc_ecc_import_private_key_ex` near `36033`. 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: 1. Both key/signature label checks in `DoUserAuthRequestEcc` and `DoUserAuthRequestEd25519` now use OR. Unequal lengths reject before `memcmp`; 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. 2. ECC `GetSize` first proves `sz <= signatureSz - i`; converting `sz` to the absolute end with `sz += i` therefore cannot wrap. Both `GetStringRef` calls use that end, not the outer field size. The subsequent `i != sz || sz != pk->signatureSz` rejection requires exact inner and outer consumption before conversion or verification. 3. Ed25519 requires `sz == pk->signatureSz - i` before 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: ```sh 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](../tests/wolf_crypto_policy/README.md). 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](https://github.com/wolfSSL/wolfssl/pull/10133.diff): removes conditional B/on-curve gating and treats `wc_ecc_import_x963_ex` input as untrusted by default in the later tree. This is not a directly applied patch to 5.8.2. - [5.9.1 tagged ChangeLog](https://raw.githubusercontent.com/wolfSSL/wolfssl/v5.9.1-stable/ChangeLog.md), Bug Fixes: explicitly recommends `WOLFSSL_VALIDATE_ECC_IMPORT` for users of older versions. This is the basis for the bounded policy choice. - [wolfSSL PR10374 diff](https://github.com/wolfSSL/wolfssl/pull/10374.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](https://github.com/wolfSSL/wolfssl/pull/10131.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.