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
+5
View File
@@ -5,5 +5,10 @@ idf_component_get_property(_sak_wolf_target wolfssl__wolfssl COMPONENT_LIB)
if(NOT TARGET "${_sak_wolf_target}") if(NOT TARGET "${_sak_wolf_target}")
message(FATAL_ERROR "wolf crypto policy: missing wolfSSL component target") message(FATAL_ERROR "wolf crypto policy: missing wolfSSL component target")
endif() endif()
# Existing upstream checks, not a vendor-source backport. PUBLIC keeps library
# and consumer settings consistent without changing the root build file.
target_compile_definitions("${_sak_wolf_target}" PUBLIC
WOLFSSL_VALIDATE_ECC_IMPORT
WOLFSSL_ECDHX_SHARED_NOT_ZERO)
target_compile_options("${_sak_wolf_target}" PUBLIC target_compile_options("${_sak_wolf_target}" PUBLIC
"-include${CMAKE_CURRENT_LIST_DIR}/wolf_crypto_policy.h") "-include${CMAKE_CURRENT_LIST_DIR}/wolf_crypto_policy.h")
+16
View File
@@ -4,6 +4,22 @@
#include <wolfssl/wolfcrypt/settings.h> #include <wolfssl/wolfcrypt/settings.h>
/* PR10133 recommends this existing check for older releases. In 5.8.2 the
* software validator must not be replaced with a successful hardware stub. */
#if !defined(HAVE_ECC) || !defined(WOLFSSL_VALIDATE_ECC_IMPORT) || \
!defined(HAVE_ECC_CHECK_KEY)
#error "wolf crypto policy: ECC requires validated imports"
#endif
#if defined(NO_ECC_CHECK_PUBKEY_ORDER) || defined(WOLF_CRYPTO_CB_ONLY_ECC) || \
defined(WOLFSSL_ATECC508A) || defined(WOLFSSL_ATECC608A) || \
defined(WOLFSSL_CRYPTOCELL) || defined(WOLFSSL_SILABS_SE_ACCEL) || \
defined(WOLFSSL_SE050) || defined(WOLFSSL_STM32_PKA)
#error "wolf crypto policy: review ECC validation backend before changing it"
#endif
#ifndef WOLFSSL_ECDHX_SHARED_NOT_ZERO
#error "wolf crypto policy: X25519 requires all-zero shared-secret rejection"
#endif
/* PR9275 selects small math on Xtensa to avoid compiler-introduced timing /* PR9275 selects small math on Xtensa to avoid compiler-introduced timing
* differences. Check resolved settings, not just command-line intentions. * differences. Check resolved settings, not just command-line intentions.
* https://github.com/wolfSSL/wolfssl/pull/9275 * https://github.com/wolfSSL/wolfssl/pull/9275
+5 -2
View File
@@ -20,8 +20,11 @@ This is a semantic map, not a complete file inventory. Start here, then read the
## Source-pinned dependency corrections (Phases 9C9D) ## Source-pinned dependency corrections (Phases 9C9D)
- Files: root `CMakeLists.txt` (after `project()`), `cmake/security_overrides.cmake`, `tools/security_overrides.py`; tests: `tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`. - Files: root `CMakeLists.txt` (after `project()`), `cmake/security_overrides.cmake`, `tools/security_overrides.py`; tests: `tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`.
- Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace seven target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, wolfSSH password bounds/payload wiping, DHCP option bounds (CVE-2026-45160), TLS 1.2 EMS error propagation (CVE-2026-50581), and X.509 OID allocation failure (CVE-2026-34874). The mbedTLS entries explicitly select validated nested `mbedtls`/`mbedx509` targets, not the component wrapper. Original notices and compile properties retained; all seven copies carry the 2026-09-15 modification notice; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin. - Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace seven target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, wolfSSH password bounds/payload wiping plus bounded IGNORE/service/string parsing, channel-window overflow rejection and ECC/Ed25519 label/exact-signature framing, DHCP option bounds (CVE-2026-45160), TLS 1.2 EMS error propagation (CVE-2026-50581), and X.509 OID allocation failure (CVE-2026-34874). The mbedTLS entries explicitly select validated nested `mbedtls`/`mbedx509` targets, not the component wrapper. Original notices and compile properties retained; all seven copies carry the 2026-09-15 modification notice; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin.
- Xtensa crypto policy: root `CMakeLists.txt` sets `CURVE25519_SMALL`/`ED25519_SMALL` before component parsing; `cmake/wolf_crypto_policy.cmake` PUBLIC-propagates `cmake/wolf_crypto_policy.h` to wolfSSL consumers. The resolved-settings guard requires both small implementations and rejects X25519 blinding and unreviewed Curve448/Ed448 enablement. Tests: `tests/wolf_crypto_policy/run.py`. [Wolf review](../wolf_security_review.md) distinguishes the implemented mitigation from pending ordering/parser/ECC review; [IDF review](../idf_security_review.md) records the three backports and remaining findings. - Xtensa crypto policy: root `CMakeLists.txt` sets `CURVE25519_SMALL`/`ED25519_SMALL` before component parsing; `cmake/wolf_crypto_policy.cmake` PUBLIC-propagates `cmake/wolf_crypto_policy.h` to wolfSSL consumers. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` enable existing P-256 import and X25519 all-zero-result checks. The resolved-settings guard requires these checks and both small implementations, rejects reviewed ECC validator-disabling/hardware-stub configurations, X25519 blinding and unreviewed Curve448/Ed448 enablement. Tests: `tests/wolf_crypto_policy/run.py` (strict actual production flags, real vendor crypto/ASN vectors and independently specified exact source deltas; candidate injection is not production evidence). [Key-validation review](../ssh_key_validation_review.md) records effective flags, caller/API limits and unmeasured validation cost. [Wolf review](../wolf_security_review.md) distinguishes implemented mitigations from unresolved ordering/deferred parsers; [IDF review](../idf_security_review.md) records the three backports and remaining findings.
- Parser tests: `tests/wolfssh_parser_contract/run.py`, [scope and exclusions](../../tests/wolfssh_parser_contract/README.md): 3,124 extracted-function cases per each of two stack modes, guard pages/UBSan traps and six rejected guard-removal mutations. Crypto doubles test gating, not arithmetic. PR892/881/880 subsets plus local signature framing only; no PR899 or ordering changes. Password/dispatch/deferred-source fences preserve prior contracts.
- Latest supplied parent build PASS: 94,340 B linked RAM / 1,768,949 B flash (+1,732 B versus 1,767,217 B). Strict crypto, parser, auth (135 cases), protocol policy and SDK override `--build-dir .pio/build/esp32-s3-devkitc-1-n16r8` commands PASS; independent review found no scoped blocker. Not target/runtime-reserve evidence.
- Ordering remains open: temporary PR793/819/840/855/921 attempt retained no changes; `SendNewKeys` WANT_WRITE/`SendExtInfo` continuation, `extInfoSent` rekey semantics and manual prerequisites unresolved. Registry 1.5.0/5.9.2 queries returned 404 on 2026-09-15 despite upstream tags. [Next strategy/commit pins](../wolf_security_review.md#ordering-blocker-and-actionable-next-strategy): isolated packaging/compatibility evaluation or prerequisite-audited source/header backport, with nonblocking/initial-KEX/rekey ordering tests before closure. No upgrade/device operation.
- Policy/evidence/limits: [library review](../security_library_review.md), [Phase 9C](../security_hardening.md#9c-library-cleanup-and-protocol-policy). Source-contract tests must locate and verify actual generated compilation inputs, not assume original vendor paths. - Policy/evidence/limits: [library review](../security_library_review.md), [Phase 9C](../security_hardening.md#9c-library-cleanup-and-protocol-policy). Source-contract tests must locate and verify actual generated compilation inputs, not assume original vendor paths.
## Secure randomness ## Secure randomness
+8
View File
@@ -2,6 +2,14 @@
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md). Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md).
## Phase 9D continuation — SSH parser / key validation — 2026-09-15
- Initial Git status clean; previous 9D work already retained. Current slice keeps managed pins and seven-source override mechanism unchanged. `tools/security_overrides.py` now bounds IGNORE/service/helper parsing, rejects window-add overflow, fixes ECC/Ed25519 label predicates, and enforces ECC nested r/s plus outer signature exact consumption and Ed25519 exact signature-field consumption. Password wipe/async and state ordering unchanged.
- `cmake/wolf_crypto_policy.*` PUBLIC-propagates `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO`, with fail-closed resolved guards. Verified prior P256 peer point reaches scalar multiplication without equivalent validation; nontrivial low-order X25519 inputs bypassed old precheck. Real vendor tests now reject these inputs. Evidence in `docs/ssh_key_validation_review.md`; generic digest/OID API hardening remains separate, no current short-digest trigger found in inspected callers.
- Parent `pio run` PASS **94,340 B linked RAM / 1,768,949 B flash** (+1,732 flash vs prior9D). Strict crypto suite initially rejected concurrent parser changes; corrected independent exact-delta expectations, not weakened provenance. Final parent five suites PASS: `wolf_crypto_policy`, `wolfssh_parser_contract` (3,124 cases in each of two stack modes, six guard-removal mutations), `wolfssh_auth_contract`135cases, `ssh_protocol_policy`, `sdk_security_overrides --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`. Independent review no scoped blockers; strict crypto/parser suites rerun PASS. Real vendor arithmetic tests and parser doubles remain separate, not live SSH transactions.
- **Ordering CVE-2025-14942 still unresolved.** Official registry queries returned404 for wolfSSH1.5.0/wolfSSL5.9.2; upstream releases exist, so a full upgrade requires deliberate pinned component integration. Temporary-only PR793/819/840/855/921 backport evaluation found manual context adaptation plus unresolved SendNewKeys WANT_WRITE / skipped SendExtInfo continuation and extInfoSent rekey semantics. No partial ordering/header-layout patch installed. Next choose coherent tested nonblocking source+header backport or upstream component integration; preserve all local auth/wiping/parser contracts. Full provenance/rekey/negative-order tests required. Details and official links in wolf review.
- Other remaining work: deferred parser/API applicability, release notices/source obligations, and whole-phase hardware validation. Review recommends a valid-but-inconsistent private/public ECC fixture and parser-to-real-crypto integration gate. No hardware/latency/resource/handshake claims, no device operations/assets/upgrades/secure-boot/encrypted-NVS changes. Do not mark Phase9 complete or require intermediate hardware signoff.
## Phase 9D — advisory mitigation / operational review — 2026-09-15 ## Phase 9D — advisory mitigation / operational review — 2026-09-15
- Work in progress; user validates Phase 9 as a whole. Secure boot/encrypted NVS excluded. No device operations or dependency upgrades. - Work in progress; user validates Phase 9 as a whole. Secure boot/encrypted NVS excluded. No device operations or dependency upgrades.
+1 -1
View File
@@ -216,7 +216,7 @@ Staged work:
1. **9A — Crash/debug build policy and operational profiles — In progress; hardware pending.** `src/security_build_policy.c` requires `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`; rejects core-dump enable/flash/UART, panic print/halt/GDBstub, runtime GDBstub and ESP/FreeRTOS debugger-aware options. `sdkconfig.defaults` makes the baseline explicit. Development/test/production use the same build baseline, not separate PlatformIO environments. Host matrix (`python3 tests/security_build_policy/run.py`) compiles the actual guard: 17 cases plus the generated-header check passed on 2026-09-15. `pio run` passed (94,196 B linked RAM / 1,828,565 B flash); target panic/recovery tests have not run. Production readiness remains pending. 1. **9A — Crash/debug build policy and operational profiles — In progress; hardware pending.** `src/security_build_policy.c` requires `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`; rejects core-dump enable/flash/UART, panic print/halt/GDBstub, runtime GDBstub and ESP/FreeRTOS debugger-aware options. `sdkconfig.defaults` makes the baseline explicit. Development/test/production use the same build baseline, not separate PlatformIO environments. Host matrix (`python3 tests/security_build_policy/run.py`) compiles the actual guard: 17 cases plus the generated-header check passed on 2026-09-15. `pio run` passed (94,196 B linked RAM / 1,828,565 B flash); target panic/recovery tests have not run. Production readiness remains pending.
2. **9B — SSH admission and credential handling — Implemented; combined target validation deferred.** Boot-lifetime, owner-only token buckets independently bound handshakes, password/signed-key requests and unsigned probes; reconnect/restart/counter clearing do not replenish them. Existing per-slot attempt limits/currentness remain. Explicit keyboard-interactive rejection, pending-signature result fencing, secret-free admission counters, consumed admin-buffer wipes and fail-closed hidden-prompt overflow/unsupported-byte handling are implemented. Four focused suites (including 35 pinned-vendor control-flow cases), 11 related regressions and `pio run` passed on 2026-09-15: 94,340 B linked RAM / 1,829,925 B flash. Global-budget starvation remains a documented tradeoff, not a solved availability problem. 2. **9B — SSH admission and credential handling — Implemented; combined target validation deferred.** Boot-lifetime, owner-only token buckets independently bound handshakes, password/signed-key requests and unsigned probes; reconnect/restart/counter clearing do not replenish them. Existing per-slot attempt limits/currentness remain. Explicit keyboard-interactive rejection, pending-signature result fencing, secret-free admission counters, consumed admin-buffer wipes and fail-closed hidden-prompt overflow/unsupported-byte handling are implemented. Four focused suites (including 35 pinned-vendor control-flow cases), 11 related regressions and `pio run` passed on 2026-09-15: 94,340 B linked RAM / 1,829,925 B flash. Global-budget starvation remains a documented tradeoff, not a solved availability problem.
3. **9C — Library cleanup and protocol policy — Implemented; combined target validation deferred.** Exact-hash build-tree overrides correct HTTPS cleanup/leaks, HTTPD scratch failure/wiping/first-read handling, bounded SSH password parsing/wiping and server-local TLS policy without modifying installed dependencies. Secure wolfSSL allocation hooks and explicit SSH policy fail closed; early web quota probing avoids receiving already-throttled bodies. TLS1.2 ECDHE-ECDSA AES-GCM and SSH GCM/modern-KEX allowlists intentionally exclude legacy-only clients; no identity migration. Bounded password/certificate/header/destructor review is documented, not exhaustive zeroization. Final build PASS 94,340 B linked RAM / 1,831,309 B flash; focused and related host/source-contract tests passed. [Review and maintenance contract](security_library_review.md). 3. **9C — Library cleanup and protocol policy — Implemented; combined target validation deferred.** Exact-hash build-tree overrides correct HTTPS cleanup/leaks, HTTPD scratch failure/wiping/first-read handling, bounded SSH password parsing/wiping and server-local TLS policy without modifying installed dependencies. Secure wolfSSL allocation hooks and explicit SSH policy fail closed; early web quota probing avoids receiving already-throttled bodies. TLS1.2 ECDHE-ECDSA AES-GCM and SSH GCM/modern-KEX allowlists intentionally exclude legacy-only clients; no identity migration. Bounded password/certificate/header/destructor review is documented, not exhaustive zeroization. Final build PASS 94,340 B linked RAM / 1,831,309 B flash; focused and related host/source-contract tests passed. [Review and maintenance contract](security_library_review.md).
4. **9D — Maintenance and lifecycle — In progress; unresolved security and distribution questions.** [Security operations](security_operations.md) documents provisioning, explicit saves, rotation, recovery, reconstruction (no implemented backup/export) and approved destructive reset/decommissioning. The [wolfSSL/wolfSSH implementation addendum](wolf_security_review.md) records the implemented Xtensa small X25519/Ed25519 mitigation (CVE-2025-12888), with consistent library/consumer flags and a resolved-settings guard; small X25519 is not combined with blinding. Server-recommended message-order work (CVE-2025-14942) and parser/ECC reviews remain pending. The [IDF implementation addendum](idf_security_review.md) records pinned DHCP (CVE-2026-45160), TLS 1.2 EMS (CVE-2026-50581) and X.509 allocation-failure (CVE-2026-34874) backports; historical research remains labeled and retained. Supplied parent build PASS: **94,340 B linked RAM / 1,767,217 B flash**, unchanged RAM / **64,092 B flash** from 9C; SDK-override, wolf-crypto-policy, wolfSSH-auth-contract and SSH-protocol-policy suites all passed. Independent reviewer reports the first two suites passed with no blocking implementation defects; see [evidence and limits](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15). The [bounded license inventory](dependency_licenses.md) marks modification/date notices resolved for all seven generated files (2026-09-15; upstream licenses retained), but radio-blob corresponding-source/exception, source/notice delivery and packaging/provenance questions remain open. Upgrades/backports require coherent source/header review, override rebasing and contract tests, not blind repinning. Broader dependency advisory coverage remains unfinished. No dependency upgrade was performed. **Phase 9 is not complete or production-ready; scoped mitigations do not establish full advisory closure, device validation or license/distribution clearance.** OTA signing trust remains separate Phase 10 work. 4. **9D — Maintenance and lifecycle — In progress; unresolved security and distribution questions.** [Security operations](security_operations.md) documents provisioning, explicit saves, rotation, recovery, reconstruction (no implemented backup/export) and approved destructive reset/decommissioning. The [wolfSSL/wolfSSH implementation addendum](wolf_security_review.md) records the implemented Xtensa small X25519/Ed25519 mitigation (CVE-2025-12888), with consistent library/consumer flags and a resolved-settings guard; small X25519 is not combined with blinding. Bounded IGNORE/service/string parsing, channel-window overflow, ECC/Ed25519 labels and exact signature framing are now corrected in the existing generated override. [P-256 import and X25519 all-zero-result checks](ssh_key_validation_review.md) are enabled with effective PUBLIC flags confirmed; generic digest/OID API hardening and deferred parser semantics remain open. **Server-recommended ordering work (CVE-2025-14942) is not fixed:** a temporary PR793/819/840/855/921 backport attempt retained no changes because manual prerequisites, nonblocking `SendNewKeys`/`WS_WANT_WRITE``SendExtInfo` continuation and `extInfoSent` rekey semantics remain unresolved. The [IDF implementation addendum](idf_security_review.md) records pinned DHCP (CVE-2026-45160), TLS 1.2 EMS (CVE-2026-50581) and X.509 allocation-failure (CVE-2026-34874) backports; historical research remains labeled and retained. Supplied parent build PASS: **94,340 B linked RAM / 1,768,949 B flash**, unchanged RAM / **+1,732 B flash** versus the preceding 1,767,217 B build. All five parent commands passed: strict wolf-crypto-policy, wolfSSH-parser-contract (3,124 cases per each of two modes plus six rejected mutations), wolfSSH-auth-contract (135 cases), SSH-protocol-policy and SDK-override with actual build-directory registration. Independent review found no blocker in these scoped changes; see [evidence and limits](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15). The [bounded license inventory](dependency_licenses.md) marks modification/date notices resolved for all seven generated files (2026-09-15; upstream licenses retained), but radio-blob corresponding-source/exception, source/notice delivery and packaging/provenance questions remain open. Official registry wolfSSH 1.5.0 / wolfSSL 5.9.2 queries returned 404 on 2026-09-15; upstream tags exist but managed compatibility is not established. The [next strategy and immutable commit references](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy) call for isolated upstream packaging/compatibility evaluation or a prerequisite-audited coherent backport, with partial-send/EXT_INFO/rekey and negative ordering tests. Upgrades/backports require coherent source/header review, override rebasing and contract tests, not blind repinning. Broader dependency advisory coverage remains unfinished. No dependency upgrade was performed. **Phase 9 is not complete or production-ready; scoped mitigations do not establish full advisory closure, device validation or license/distribution clearance.** OTA signing trust remains separate Phase 10 work.
At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not required between implementation slices. [Security hardening](security_hardening.md) collects profiles, host evidence and the combined target checklist. Silent panic reboot removes useful crash diagnostics, not ordinary reset/boot/status information or every possible log disclosure. Raw flash/RAM/dumps remain secret-bearing, not routine diagnostic exports. Existing coredump bytes are not retroactively cleared; no secure erase is claimed. Isolated synthetic-secret debug builds require explicit reviewed source-policy changes, not a provided bypass flag. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not required between implementation slices. [Security hardening](security_hardening.md) collects profiles, host evidence and the combined target checklist. Silent panic reboot removes useful crash diagnostics, not ordinary reset/boot/status information or every possible log disclosure. Raw flash/RAM/dumps remain secret-bearing, not routine diagnostic exports. Existing coredump bytes are not retroactively cleared; no secure erase is claimed. Isolated synthetic-secret debug builds require explicit reviewed source-policy changes, not a provided bypass flag.
+8 -6
View File
@@ -105,9 +105,10 @@ The existing global five-verifications/60-second fixed window is unchanged: malf
## 9D maintenance and lifecycle — in progress ## 9D maintenance and lifecycle — in progress
- [Security operations](security_operations.md) supplies source-checked provisioning, explicit-save, account/key/identity rotation, recovery, reconstruction and approved erase/decommissioning procedures. There is no implemented configuration backup/export or private-identity restore workflow. Commands and checklists are not device execution evidence; the user will validate Phase 9 as a whole. - [Security operations](security_operations.md) supplies source-checked provisioning, explicit-save, account/key/identity rotation, recovery, reconstruction and approved erase/decommissioning procedures. There is no implemented configuration backup/export or private-identity restore workflow. Commands and checklists are not device execution evidence; the user will validate Phase 9 as a whole.
- [wolfSSL/wolfSSH review and implementation addendum](wolf_security_review.md): the **CVE-2025-12888** Xtensa mitigation now selects `CURVE25519_SMALL` and `ED25519_SMALL` consistently for wolfSSL and consumers. The PUBLIC forced-include resolved-settings guard rejects missing small implementations, X25519 blinding (incompatible with small in this pinned version), and unreviewed Curve448/Ed448 enablement. **Still open:** server-recommended message-order correction for **CVE-2025-14942**, parser/ECC review and broader API/feature applicability. No server credential-leak or invalid-curve exploit is demonstrated; target timing/interoperability remains untested. - [wolfSSL/wolfSSH review and implementation addendum](wolf_security_review.md): the **CVE-2025-12888** Xtensa mitigation now selects `CURVE25519_SMALL` and `ED25519_SMALL` consistently for wolfSSL and consumers. The PUBLIC forced-include resolved-settings guard rejects missing small implementations, X25519 blinding (incompatible with small in this pinned version), and unreviewed Curve448/Ed448 enablement. The existing generated wolfSSH override now bounds IGNORE/service strings, rejects zero-capacity string output and channel-window overflow, corrects ECC/Ed25519 key/signature labels, and enforces exact signature framing (ECC nested r/s bounds plus inner/outer consumption; Ed25519 outer consumption). These are PR892/881/880 subsets plus local framing corrections, not full backports; password wiping/async retention remain unchanged. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` now enable P-256 import validation and X25519 all-zero-result rejection, with effective production flags confirmed and fail-closed backend guards. See [key-validation evidence](ssh_key_validation_review.md) and [parser scope/limits](../tests/wolfssh_parser_contract/README.md). **Still open:** server-recommended message-order correction for **CVE-2025-14942**, PR899/deferred parsers, service/key-blob semantics and broader API applicability. Current raw SSH digest construction does not expose the reviewed short-digest/OID trigger; generic PR10131 API hardening is not backported. No exploit or whole-library clearance is demonstrated; added validation CPU/allocation cost and target interoperability remain unmeasured.
- [Focused IDF review and implementation addendum](idf_security_review.md): pinned backports now implement DHCP option bounds (**CVE-2026-45160**), TLS 1.2 EMS failure return (**CVE-2026-50581**) and X.509 OID allocation-failure handling (**CVE-2026-34874**). Explicit nested-target validation places the mbedTLS edits on `mbedtls`/`mbedx509`, retaining source properties and exactly-one-source checks. WS negotiation **CVE-2026-45541** and ASN.1 named-data **CVE-2025-48965** remain unpatched with the review's qualified applicability, not blanket closure. - [Focused IDF review and implementation addendum](idf_security_review.md): pinned backports now implement DHCP option bounds (**CVE-2026-45160**), TLS 1.2 EMS failure return (**CVE-2026-50581**) and X.509 OID allocation-failure handling (**CVE-2026-34874**). Explicit nested-target validation places the mbedTLS edits on `mbedtls`/`mbedx509`, retaining source properties and exactly-one-source checks. WS negotiation **CVE-2026-45541** and ASN.1 named-data **CVE-2025-48965** remain unpatched with the review's qualified applicability, not blanket closure.
- [Dependency license inventory](dependency_licenses.md) is a bounded engineering inventory, not legal or distribution clearance. All seven generated sources now carry prominent modification notices dated **2026-09-15**, with upstream notices retained, including both mbedTLS dual-license headers. That narrow finding is resolved. Radio-blob corresponding-source/exception questions, actual firmware/device/browser notice delivery, preferred-source packaging, wolfSSH package-license discrepancy and icon provenance/full-license work remain open. Existing repository notices alone are not a complete release bundle. - [Dependency license inventory](dependency_licenses.md) is a bounded engineering inventory, not legal or distribution clearance. All seven generated sources now carry prominent modification notices dated **2026-09-15**, with upstream notices retained, including both mbedTLS dual-license headers. That narrow finding is resolved. Radio-blob corresponding-source/exception questions, actual firmware/device/browser notice delivery, preferred-source packaging, wolfSSH package-license discrepancy and icon provenance/full-license work remain open. Existing repository notices alone are not a complete release bundle.
- **Ordering is not fixed.** A temporary coherent PR793/819/840/855/921 backport attempt retained no changes: manual prerequisites and nonblocking `SendNewKeys`/`WS_WANT_WRITE` skipping `SendExtInfo` continuation, plus `extInfoSent` rekey semantics, remain unresolved. Official registry 1.5.0/5.9.2 queries returned 404 on 2026-09-15 despite upstream tags existing. Follow the [actionable ordering strategy](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy): evaluate immutable upstream snapshots with reviewed packaging or audit a complete prerequisite-aware backport; require partial-send/EXT_INFO/initial-KEX/rekey and negative ordering tests before closure.
- No dependency versions were upgraded. Beyond the implemented backports above, proposed upgrades/backports remain candidates, not approved compatible versions. Re-audit coherent source/header changes, effective compile policy, exact-hash overrides and callback/parser contracts, then obtain host/build and whole-phase target evidence. 9A9C passes below are historical scoped evidence, not closure of these newly recorded findings. - No dependency versions were upgraded. Beyond the implemented backports above, proposed upgrades/backports remain candidates, not approved compatible versions. Re-audit coherent source/header changes, effective compile policy, exact-hash overrides and callback/parser contracts, then obtain host/build and whole-phase target evidence. 9A9C passes below are historical scoped evidence, not closure of these newly recorded findings.
Secure boot and encrypted NVS remain excluded. No runbook, advisory report or license inventory establishes production readiness or authorizes a destructive device operation. Secure boot and encrypted NVS remain excluded. No runbook, advisory report or license inventory establishes production readiness or authorizes a destructive device operation.
@@ -130,18 +131,19 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver
### Mitigation host/build evidence — 2026-09-15 ### Mitigation host/build evidence — 2026-09-15
Supplied parent results (not rerun for this documentation update): `pio run` **PASS**, **94,340 B linked RAM / 1,767,217 B flash**. Against the historical 9C build below, RAM is unchanged and flash is **64,092 B smaller**. Linked size is not runtime headroom or timing evidence. Supplied parent results (not rerun for this documentation update): `pio run` **PASS**, **94,340 B linked RAM / 1,768,949 B flash**. RAM is unchanged and flash is **1,732 B larger** than the preceding 1,767,217 B mitigation build. Linked size is not runtime headroom or timing evidence.
All four parent commands passed: All five parent commands passed (crypto policy in strict mode, without candidate injection):
```sh ```sh
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8 CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8
CCACHE_DISABLE=1 python3 tests/wolf_crypto_policy/run.py CCACHE_DISABLE=1 python3 tests/wolf_crypto_policy/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py
CCACHE_DISABLE=1 python3 tests/ssh_protocol_policy/run.py CCACHE_DISABLE=1 python3 tests/ssh_protocol_policy/run.py
``` ```
The independent reviewer reports the first two suites passed with no blocking implementation defects. Documentation verification separately matched all seven existing generated files to pinned originals plus checked-in edits/notices and checked retained mbedTLS license headers. This is scoped implementation/host/build evidence, not completed wolfSSH ordering/parser/ECC review, license packaging clearance or whole-phase acceptance. No target evidence, upgrade, asset regeneration or device operation is claimed. Independent review found no blocker in the scoped changes. The parser suite passed **3,124 cases per stack mode (two modes)** with guard pages/UBSan trap instrumentation and **six rejected guard-removal mutations**; its crypto doubles establish parser gating, not signature arithmetic. The auth suite passed **135 cases**. Strict crypto tests run real vendor arithmetic/ASN vectors, independently compare seven audited source bodies with exact parser deltas, and check twelve production translation units plus negative policy cases. Effective ECC/X25519 flags were confirmed. SDK override validation includes actual seven-source build registration. This is scoped implementation/host/build evidence, not ordering closure, exhaustive parser/crypto review, license clearance or whole-phase acceptance. No target evidence, upgrade, asset regeneration or device operation is claimed.
### Host and build — historical passes 2026-09-15 (9A9C) ### Host and build — historical passes 2026-09-15 (9A9C)
@@ -189,14 +191,14 @@ Retain these checks for the user's final whole-phase test session; do not stop i
#### Protocol compatibility and allocation-failure recovery #### Protocol compatibility and allocation-failure recovery
1. Verify both allowed TLS suites and both SSH GCM ciphers using compatible clients; force excluded CBC/CTR/other-only offers and confirm rejection. Exercise both SSH KEX choices and both user-key types, initial handshake and rekey, plus TLS renegotiation rejection. Retain UART0 access; do not rotate identities to work around an algorithm mismatch. 1. Verify both allowed TLS suites and both SSH GCM ciphers using compatible clients; force excluded CBC/CTR/other-only offers and confirm rejection. Exercise both SSH KEX choices and both user-key types, initial handshake and rekey, plus TLS renegotiation rejection. Retain UART0 access; do not rotate identities to work around an algorithm mismatch.
2. With synthetic credentials, test truncated/oversized SSH password and change-password packets: no authentication callback for malformed fields, no crash, bounded disconnect/recovery. Host canary assertions are not real encrypted-packet coverage. 2. With synthetic credentials, test truncated/oversized SSH password and change-password packets: no authentication callback for malformed fields, no crash, bounded disconnect/recovery. Include malformed IGNORE/service strings, window overflow, ECC/Ed25519 labels and nested/trailing signature bytes, invalid P-256 points and low-order X25519 inputs. Measure added import-validation latency/allocation/stack cost, host-key loading and handshake deadlines under repeated KEX/rekey and combined load. Host canary/vector assertions are not real encrypted-packet coverage.
3. Exercise HTTPS failed-start, post-handshake allocation failure, normal/failed-stop retry and split-header scratch allocation failure on a separately reviewed fault-injection image. Observe recovery/no accumulating allocation loss without exporting keys or RAM. Failed stop must not prematurely free live TLS state. 3. Exercise HTTPS failed-start, post-handshake allocation failure, normal/failed-stop retry and split-header scratch allocation failure on a separately reviewed fault-injection image. Observe recovery/no accumulating allocation loss without exporting keys or RAM. Failed stop must not prematurely free live TLS state.
4. Repeatedly start/stop HTTPS and SSH and stress header parsing/authentication under the full transport mix. Capture internal/DMA/PSRAM free/minimum/largest-block and stack margins alongside serial/broker loss counters. Specifically measure old-plus-new allocation peaks and secure-free CPU cost; previous very low internal minima remain important. 4. Repeatedly start/stop HTTPS and SSH and stress header parsing/authentication under the full transport mix. Capture internal/DMA/PSRAM free/minimum/largest-block and stack margins alongside serial/broker loss counters. Specifically measure old-plus-new allocation peaks and secure-free CPU cost; previous very low internal minima remain important.
5. Verify exhausted web login returns early without stalled-body work, clears the used pre-login challenge, and recovers after the documented window. Check malformed requests below quota and correct credentials for normal behavior; do not infer fairness from a rate-limit pass. 5. Verify exhausted web login returns early without stalled-body work, clears the used pre-login challenge, and recovers after the documented window. Check malformed requests below quota and correct credentials for normal behavior; do not infer fairness from a rate-limit pass.
## Staged next work ## Staged next work
- **Continue 9D maintenance and lifecycle.** Resolve the [open advisory priorities](wolf_security_review.md#release-decision--current-path-priorities), finish broader dependency coverage beyond the bounded IDF/mbedTLS review and three implemented backports, and address the [release source/notice work](dependency_licenses.md#actionable-release-work-not-performed). Runbooks are documented, not rehearsed; remaining mitigations/reviews, distribution clearance and whole-phase acceptance remain outstanding. - **Continue 9D maintenance and lifecycle.** Execute the [ordering strategy and remaining advisory work](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy), finish broader dependency coverage beyond the bounded IDF/mbedTLS review and three implemented backports, and address the [release source/notice work](dependency_licenses.md#actionable-release-work-not-performed). Runbooks are documented, not rehearsed; remaining mitigations/reviews, distribution clearance and whole-phase acceptance remain outstanding.
- **Retained evidence limits:** 9C completes a bounded cleanup/protocol review, not every-library-copy zeroization. Live inline residue, compaction tails, hardware/stack intermediates, global admission starvation and resource/interop measurements remain documented limitations or combined target gates. Any additional hardening must preserve owner lifetimes and bounded recovery. - **Retained evidence limits:** 9C completes a bounded cleanup/protocol review, not every-library-copy zeroization. Live inline residue, compaction tails, hardware/stack intermediates, global admission starvation and resource/interop measurements remain documented limitations or combined target gates. Any additional hardening must preserve owner lifetimes and bounded recovery.
- **Phase 10: OTA trust.** Define independent image-signature verification, trust-anchor provisioning, rotation/revocation, rollback/downgrade and recovery policy without secure boot. Authenticated transport alone is not image-signing policy, and OTA signature checks cannot prevent physical firmware replacement. - **Phase 10: OTA trust.** Define independent image-signature verification, trust-anchor provisioning, rotation/revocation, rollback/downgrade and recovery policy without secure boot. Authenticated transport alone is not image-signing policy, and OTA signature checks cannot prevent physical firmware replacement.
+112
View File
@@ -0,0 +1,112 @@
# 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:1125111317`, `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:1070911020` parses X9.63 coordinates and selects the curve. At `1099310996`, `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` (`46694752`) 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` (`1049510703`) 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:1133511394`, `KeyAgreeCurve25519_server`, explicitly runs `wc_curve25519_check_public` before import, key generation and shared-secret calculation. Installed `curve25519.c:645710` 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` (`452536`) 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.
+15 -3
View File
@@ -4,13 +4,25 @@
**Xtensa small-math mitigation implemented; the broader review and security sign-off remain open.** Root `CMakeLists.txt` defines `CURVE25519_SMALL` and `ED25519_SMALL` before component parsing. `cmake/wolf_crypto_policy.cmake` propagates a forced-include resolved-settings guard PUBLIC from wolfSSL to consumers, including wolfSSH/application code. `cmake/wolf_crypto_policy.h` requires both enabled small implementations, rejects `WOLFSSL_CURVE25519_BLINDING`, and rejects enabling Curve448/Ed448 without review. This follows PR 9275's small-math policy for the enabled Xtensa algorithms, with consistent library/consumer production flags and ABI-sensitive layouts. **Blinding is not enabled with small X25519**: wolfSSL 5.8.2 excludes/rejects this combination; do not force it back on or present the historical blinding observation below as current policy. **Xtensa small-math mitigation implemented; the broader review and security sign-off remain open.** Root `CMakeLists.txt` defines `CURVE25519_SMALL` and `ED25519_SMALL` before component parsing. `cmake/wolf_crypto_policy.cmake` propagates a forced-include resolved-settings guard PUBLIC from wolfSSL to consumers, including wolfSSH/application code. `cmake/wolf_crypto_policy.h` requires both enabled small implementations, rejects `WOLFSSL_CURVE25519_BLINDING`, and rejects enabling Curve448/Ed448 without review. This follows PR 9275's small-math policy for the enabled Xtensa algorithms, with consistent library/consumer production flags and ABI-sensitive layouts. **Blinding is not enabled with small X25519**: wolfSSL 5.8.2 excludes/rejects this combination; do not force it back on or present the historical blinding observation below as current policy.
The CVE-2025-12888 configuration mitigation is no longer merely proposed. Supplied parent evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,767,217 B flash**, **64,092 B flash** from 9C with unchanged linked RAM; all four [focused mitigation commands](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15) passed. Independent reviewer reports SDK-override and wolf-crypto-policy tests passed with no blocking implementation defects. These are supplied host/build results, not reruns by this documentation update, device timing measurements, real SSH interoperability/rekey evidence or runtime-headroom evidence. **Parser corrections implemented:** `tools/security_overrides.py` extends the existing hash-pinned wolfSSH generated source (still seven overridden files overall). The PR892 subset bounds `DoIgnore`/`GetSkip` and service-string reads, accepts boundary-empty skips, rejects zero-capacity `GetString`, and preserves the old strict service-name length limit. The PR881 subset rejects channel-window addition overflow without changing the window. The PR880 subset changes both ECC/Ed25519 key/signature label comparisons to short-circuit OR. Local framing corrections bound ECC r/s reads to the declared sub-blob and require exact inner/outer consumption; Ed25519 also requires exact outer consumption. Valid framing and crypto/digest/message construction remain unchanged; formerly tolerated malformed labels/trailing bytes reject. Existing password bounds/wiping and async-pending retention remain intact. See the [parser contract and deferred scope](../tests/wolfssh_parser_contract/README.md); these are subsets, not complete PR backports. PR899 has no applied hunks; service semantics, other parsers and standalone ECC key-blob semantics remain open.
**Still pending:** coherent wolfSSH message-order review/correction (CVE-2025-14942), IGNORE/service and other parser review, ECC import/ECDH/raw-signature validation review, remaining advisory applicability, and whole-phase target validation. No dependency upgrades were made; newer release pairs below remain unvalidated candidates. License/source/notice packaging remains unresolved in the [license inventory](dependency_licenses.md). Phase 9 is not complete or production-ready. **Crypto validation implemented:** PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` enable existing upstream P-256 import validation and X25519 all-zero-result rejection. The guard rejects missing checks and reviewed validator-disabling/hardware-stub configurations. Strict production checks confirm the effective flags without candidate injection. The [key-validation review](ssh_key_validation_review.md) traces the previously missing unauthenticated P-256 KEX point check and low-order X25519 inputs that pass the old precheck. Current raw SSH signature callers supply locally derived full digests; no current short-digest/OID-confusion trigger was found. This does not backport generic PR10131 API hardening or demonstrate an exploit. Import validation adds CPU/allocation cost requiring target measurement.
Supplied parent evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,768,949 B flash**, unchanged RAM / **+1,732 B flash** versus the preceding 1,767,217 B mitigation build. All five [focused commands](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15) passed: strict crypto policy, parser contract (3,124 cases in each of two stack modes plus six rejected guard-removal mutations), auth contract (135 cases), SSH protocol policy, and SDK overrides with actual build-directory registration. Independent review found no blocker in the scoped changes. Parser tests use crypto doubles; crypto tests execute real vendor arithmetic and independently check exact generated-source deltas. Neither establishes an end-to-end SSH exchange. These are supplied results, not build/test reruns by this documentation update or runtime-headroom evidence.
**Still pending:** coherent wolfSSH ordering correction (CVE-2025-14942), deferred parser/API/advisory review, whole-phase target validation and [license/source packaging](dependency_licenses.md). No dependency upgrade or device operation was performed. Phase 9 is not complete or production-ready.
### Ordering blocker and actionable next strategy
The supplied follow-up reports an attempted coherent PR793/819/840/855/921 backport in temporary work only; **no ordering changes were retained**. Manual patch prerequisites remained unresolved, as did nonblocking `SendNewKeys` returning `WS_WANT_WRITE` and skipping the `SendExtInfo` continuation, and `extInfoSent` semantics across rekey. Parser/crypto passes do not close these state-machine issues.
Official registry queries for wolfSSH **1.5.0** and wolfSSL **5.9.2** returned **404 on 2026-09-15**. Upstream tags exist at wolfSSH commit `8643d7be841184f766374e3b0ed68ced6391543c` and wolfSSL commit `ac01707f552c611fbd135cc723b2682b3e7f80f2`; tag existence is not managed-component availability or ESP compatibility. This is supplied query evidence, not a fresh network check by this documentation update.
Next, evaluate those immutable upstream snapshots in an isolated compatibility branch/worktree with an explicit reviewed packaging/provenance plan, rather than assume a registry version bump works. Alternatively, inventory and review every prerequisite of a coherent source/header backport before applying it. In either approach, first add state-machine regression coverage for partial sends/`WS_WANT_WRITE`, exactly-once EXT_INFO continuation, initial KEX versus rekey and `extInfoSent` lifetime, unexpected/pre-auth messages and valid client flows. Rebase source overrides and version/callback contracts explicitly; preserve password wiping and parser/crypto checks, then rerun focused suites and the firmware build. Only after review and whole-phase target interoperability/resource tests may ordering closure be claimed.
## Historical pre-mitigation research baseline — 2026-09-15 ## Historical pre-mitigation research baseline — 2026-09-15
**The remainder retains the original research evidence. “Current” macros, generated hashes and “not applied/tested” statements below refer to the earlier non-small snapshot; the addendum supersedes those implementation-status claims only. The pending ordering/parser/ECC findings are not closed.** **The remainder retains the original pre-mitigation research evidence. “Current” macros, generated hashes, priorities and “not applied/tested” statements below describe that historical snapshot, not today's implementation. The addendum and linked key-validation review supersede the scoped small-math, parser and ECC/X25519 status claims. Ordering and explicitly deferred findings remain open.**
Review date: **2026-09-15**. Read-only external research and local applicability inspection; stopped at the user's requested handoff. **Not a completed security review or release clearance.** Only this new report was written. No dependencies, sources, generated inputs, or parent documents were changed; no build, hardware test, network exploit, or public PoC was executed. Review date: **2026-09-15**. Read-only external research and local applicability inspection; stopped at the user's requested handoff. **Not a completed security review or release clearance.** Only this new report was written. No dependencies, sources, generated inputs, or parent documents were changed; no build, hardware test, network exploit, or public PoC was executed.
+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 Project-owned configuration for pinned wolfSSL 5.8.2~1 / wolfSSH 1.4.20;
wolfSSH 1.4.20 pins and unmodified managed sources. This is not an upstream no installed vendor edits, dependency upgrades, generated override edits or
upgrade, blanket security clearance, or mitigation of other listed advisories. 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 - Existing root definitions `CURVE25519_SMALL` / `ED25519_SMALL` follow
[PR9275 files](https://api.github.com/repos/wolfSSL/wolfssl/pulls/9275/files) [PR9275](https://github.com/wolfSSL/wolfssl/pull/9275)'s Xtensa mitigation.
([PR](https://github.com/wolfSSL/wolfssl/pull/9275), head reported by the files Small math is incompatible with this version's X25519 blinding; do not mix
API: `c161cbd9f3fa1247382bb5b6269c7379222cabf5`). Its `settings.h` patch ABI-sensitive library and consumer settings. Curve448/Ed448 require review.
selects `CURVE25519_SMALL`, `ED25519_SMALL`, `CURVE448_SMALL`, and `ED448_SMALL` - `cmake/wolf_crypto_policy.cmake` now PUBLIC-defines
under `__xtensa__`: Xtensa compilers have generated non-constant-time assembly `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO`, enabling
from the fast C implementation; upstream says the small implementation is not existing upstream P256 import and X25519 result checks. The root already
known to have those issues. This is upstream mitigation guidance, not proof of includes this module; no root edit is needed.
constant-time execution on our compiler/device. - The PUBLIC forced-include guard checks resolved settings and rejects missing
requirements and known ECC validator-disabling/hardware-stub configurations.
Installed `include/user_settings.h` enables X25519 and Ed25519. Installed Existing RNG callback and software AES/SHA controls are unchanged.
`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.
## Commands ## Commands
From the repository root, after the parent regenerates/builds the firmware:
```sh ```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 python3 tests/wolf_crypto_policy/run.py
``` ```
Optional explicit database: Optional database argument:
```sh ```sh
python3 tests/wolf_crypto_policy/run.py --compile-commands .pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json 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 Candidate mode injects all four policy definitions and the guard into saved
uses their actual compiler, include paths and definitions without adding small commands. It is **not production configuration/build evidence**. Strict mode
flags. Missing/ambiguous entries, absent policy, wrong architecture, incompatible injects nothing, and must fail with stale commands lacking the new flags.
macros, compiler errors and failed vectors fail the test. It preprocesses and No mode runs PlatformIO, regenerates overrides or communicates with a device.
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.
Before the parent reconfigures, explicitly test the candidate using old commands: ## Coverage
```sh All modes:
python3 tests/wolf_crypto_policy/run.py --candidate
```
This injects the two small flags and the guard and labels its output **CANDIDATE - 20 fail-closed guard matrix cases.
replay**, not production configuration evidence. It does not run CMake/PlatformIO. - A temporary CMake project includes the actual production policy module and
Host-only subset: 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 Target modes additionally:
python3 tests/wolf_crypto_policy/run.py --host-only
```
All modes run eight guard matrix cases and compile the installed vendor small - Pin original wolfSSH `internal.c` and wolfCrypt `ecc.c`, `curve25519.c`,
implementations into a temporary host executable: RFC7748 section 6.1 X25519 `signature.c`; locate the actual generated wolfSSH compile input and compare
shared secret, RFC8032 section 7.1 test 1 Ed25519 empty-message verification, and seven complete audited crypto/auth/hash function bodies: five must remain
rejection of a corrupted signature. Host settings are deliberately minimal, identical, while ECC/Ed25519 authentication must match independently specified
with streaming verification enabled and unused functions garbage-collected; exact label/framing deltas reconstructed from the hash-pinned original, with
they are not the ESP-IDF runtime/entropy/hardware configuration. No synthetic exact anchor counts. Expectations are not imported from the generator. Any
implementation substitutes for the tested arithmetic. Requirements: Python 3, additional change requires re-audit, not repinning or skipping a body. This
`cc`/linker, installed managed component; target checks also require the existing does not validate the entire override generator. Generated hash is printed.
Xtensa toolchain, generated headers and compile database. Commands are bounded; - Replay actual Xtensa compiler/includes for macro and syntax checks of twelve
temporary outputs are removed automatically. 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, ## Evidence and remaining gates — 2026-09-15
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.
The parent must run the normal full build and then strict mode above. A build was Follow-up strict production run PASS without candidate injection, including all
explicitly not run for this task. Existing compile-database success alone would host tests (20 guard cases, three-target CMake propagation, real crypto and ASN
not prove the linked/flashed image matches it. No device operations were run. vectors), seven complete source-body comparisons with reviewed exact parser
Still required: target SSH X25519 negotiation, Ed25519 authentication, rekey, deltas, twelve target macro/syntax checks and four negative target-settings
combined service load, stack/heap reserves and handshake latency/deadline checks. cases. Earlier candidate and host-only runs also passed; the final ASN-decode
Small implementations may reduce performance; no target timing, side-channel cases passed in candidate and strict runs.
measurement, interoperability or resource claim is made. Host vectors are narrow Earlier development runs required correcting fixture settings/linkage; they
correctness checks, not exhaustive cryptographic validation. 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 #!/usr/bin/env python3
"""Offline compile-profile regression; never invokes PlatformIO or a device.""" """Offline compile-profile regression; never invokes PlatformIO or a device."""
import argparse import argparse
import hashlib
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -15,6 +16,11 @@ GUARD = ROOT / 'cmake/wolf_crypto_policy.h'
VENDOR = ROOT / 'managed_components/wolfssl__wolfssl' VENDOR = ROOT / 'managed_components/wolfssl__wolfssl'
ENV = {**os.environ, 'CCACHE_DISABLE': '1'} ENV = {**os.environ, 'CCACHE_DISABLE': '1'}
SMALL = ('CURVE25519_SMALL', 'ED25519_SMALL') 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): def run(args, cwd=ROOT, **kw):
@@ -48,11 +54,12 @@ def matrix():
settings = tmp / 'wolfssl/wolfcrypt/settings.h' settings = tmp / 'wolfssl/wolfcrypt/settings.h'
settings.parent.mkdir(parents=True) settings.parent.mkdir(parents=True)
settings.write_text('/* Resolved settings supplied by matrix. */\n') 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 = [('valid', base, True)]
cases += [(f'missing {m}', [x for x in base if x != m], False) for m in base] 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 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: for label, defines, good in cases:
p = run(['cc', '-x', 'c', '-fsyntax-only', '-I' + str(tmp), p = run(['cc', '-x', 'c', '-fsyntax-only', '-I' + str(tmp),
'-include', str(GUARD), *['-D' + x for x in defines], '-'], input='') '-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') 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): def profiles(database, candidate):
entries = json.loads(database.read_text()) 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_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', 'wolfssl__wolfssh/src/ssh.c',
'security_overrides/wolfssh_internal/internal.c', 'security_overrides/wolfssh_internal/internal.c',
'src/ssh_transport.c', 'src/ssh_security.c') 'src/ssh_transport.c', 'src/ssh_security.c')
@@ -76,34 +217,38 @@ def profiles(database, candidate):
entry = matches[0] entry = matches[0]
command = clean(entry) command = clean(entry)
if candidate: 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): elif not any('wolf_crypto_policy.h' in x for x in command):
raise RuntimeError(f'{suffix}: missing production guard; parent must reconfigure/build') raise RuntimeError(f'{suffix}: missing production guard; parent must reconfigure/build')
text = require(run(command + ['-E', '-dM'], cwd=entry['directory'])) text = require(run(command + ['-E', '-dM'], cwd=entry['directory']))
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', text, re.M)) 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', 'WC_RNG_SEED_CB', 'WOLFSSL_ED25519_STREAMING_VERIFY',
'NO_WOLFSSL_ESP32_CRYPT_AES', 'NO_WOLFSSL_ESP32_CRYPT_HASH'): 'NO_WOLFSSL_ESP32_CRYPT_AES', 'NO_WOLFSSL_ESP32_CRYPT_HASH'):
if name not in macros: if name not in macros:
raise RuntimeError(f'{suffix}: missing resolved {name}') raise RuntimeError(f'{suffix}: missing resolved {name}')
if not any(x in macros for x in ('__XTENSA__', '__xtensa__')): if not any(x in macros for x in ('__XTENSA__', '__xtensa__')):
raise RuntimeError('expected actual Xtensa compiler') 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: if name in macros:
raise RuntimeError(f'{suffix}: unexpected {name}') raise RuntimeError(f'{suffix}: unexpected {name}')
require(run(command + ['-fsyntax-only'], cwd=entry['directory'])) require(run(command + ['-fsyntax-only'], cwd=entry['directory']))
print(f'PASS: {"CANDIDATE replay" if candidate else "production"} macros + syntax: {suffix}') print(f'PASS: {"CANDIDATE replay" if candidate else "production"} macros + syntax: {suffix}')
# Exercise failures with the real installed settings, not only fake headers. # Exercise failures with the real installed settings, not only fake headers.
entry = matches[0] 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] and 'wolf_crypto_policy.h' not in x]
for missing in SMALL: for missing in POLICY:
command = base + ['-D' + x for x in SMALL if x != missing] command = base + ['-D' + x for x in POLICY if x != missing]
command += ['-U' + missing, '-include', str(GUARD), '-E'] command += ['-U' + missing, '-include', str(GUARD), '-E']
p = run(command, cwd=entry['directory']) p = run(command, cwd=entry['directory'])
if p.returncode == 0 or 'wolf crypto policy:' not in p.stderr: if p.returncode == 0 or 'wolf crypto policy:' not in p.stderr:
raise RuntimeError(f'real settings accepted missing {missing}: {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(): def vectors():
@@ -128,6 +273,9 @@ def vectors():
#define NO_WRITEV #define NO_WRITEV
#define NO_DEV_RANDOM #define NO_DEV_RANDOM
#define NO_MAIN_DRIVER #define NO_MAIN_DRIVER
#define HAVE_ECC
#define WOLFSSL_VALIDATE_ECC_IMPORT
#define WOLFSSL_ECDHX_SHARED_NOT_ZERO
#define HAVE_CURVE25519 #define HAVE_CURVE25519
#define HAVE_ED25519 #define HAVE_ED25519
#define CURVE25519_SMALL #define CURVE25519_SMALL
@@ -144,6 +292,33 @@ def vectors():
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources], *[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
'-Wl,--gc-sections', '-o', str(tmp / 'vectors')])) '-Wl,--gc-sections', '-o', str(tmp / 'vectors')]))
print(require(run([str(tmp / 'vectors')])).strip()) 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(): def main():
@@ -155,8 +330,10 @@ def main():
parser.add_argument('--host-only', action='store_true') parser.add_argument('--host-only', action='store_true')
args = parser.parse_args() args = parser.parse_args()
matrix() matrix()
cmake_propagation()
vectors() vectors()
if not args.host_only: if not args.host_only:
source_contract(args.compile_commands)
profiles(args.compile_commands, args.candidate) profiles(args.compile_commands, args.candidate)
+22 -1
View File
@@ -3,6 +3,7 @@
#include <string.h> #include <string.h>
#include <wolfssl/wolfcrypt/curve25519.h> #include <wolfssl/wolfcrypt/curve25519.h>
#include <wolfssl/wolfcrypt/ed25519.h> #include <wolfssl/wolfcrypt/ed25519.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#define CHECK(x) do { if (!(x)) { \ #define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "vector failure at line %d: %s\n", __LINE__, #x); return 1; \ 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_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(wc_curve25519_shared_secret_ex(&alice, &bob, result, &size, EC25519_LITTLE_ENDIAN) == 0);
CHECK(size == 32 && memcmp(result, expected, 32) == 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(&alice);
wc_curve25519_free(&bob); wc_curve25519_free(&bob);
@@ -51,6 +72,6 @@ int main(void)
(void)wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key); (void)wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key);
CHECK(valid == 0); CHECK(valid == 0);
wc_ed25519_free(&key); 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; return 0;
} }
+130
View File
@@ -0,0 +1,130 @@
# Bounded wolfSSH parser contract
Run from the project root (installed pinned sources and a host C compiler required):
```sh
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
```
No download, PlatformIO, managed-component edit, production build-tree regeneration,
or device operation is performed. The runner verifies the original `internal.c`
SHA-256, calls the production `render_entry`, writes and reads back its generated
bytes in a temporary directory, and extracts complete actual functions. C tests
run with guard pages and UBSan trap instrumentation, both with and without
`WOLFSSH_SMALL_STACK`. The existing SDK suite separately tests generation and
CMake source replacement using fixtures. This is **not** a claim that an existing
production generated file or firmware binary contains these edits.
## Reviewed upstream evidence and exact implementation scope
Official diffs fetched and inspected on 2026-09-15:
- https://github.com/wolfSSL/wolfssh/pull/892.diff
- https://github.com/wolfSSL/wolfssh/pull/881.diff
- https://github.com/wolfSSL/wolfssh/pull/899.diff
- https://github.com/wolfSSL/wolfssh/pull/880.diff
These are PR URLs, not immutable commit pins. The authoritative local inputs remain
original wolfSSH **1.4.20**, SHA-256
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`,
plus the exact-once checked-in edits in `tools/security_overrides.py`. No repinning
or wholesale upstream patch application occurs.
Covered:
- **892 subset:** `DoIgnore` calls `GetSkip`; `GetSkip` uses `GetSize` and accepts
an empty string ending exactly at the payload boundary. `DoServiceRequest`
validates the full string with `GetSize` before `GetString`, retaining the old
strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior.
Failure leaves the caller index and client state untouched. Successful state
transition remains exactly the old one; service-name semantic validation is
not added. The original `GetSize` already uses bounded subtraction and needs
no change. `GetString` now uses it and rejects zero output capacity before
subtraction/copy; ordinary bounded truncation semantics remain unchanged.
- **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit
maximum with `WS_OVERFLOW_E`, leaving the channel window unchanged. The parsed
index still advances, as upstream does. No new include is needed for the
explicit word32 maximum. Unknown channels and truncated fields stay rejected.
- **880 subset:** both key/signature type checks in `DoUserAuthRequestEcc` and
`DoUserAuthRequestEd25519` use OR.
`GetSize` bounds the incoming span first; unequal lengths short-circuit before
`memcmp`, and equal lengths compare exactly the expected span. Valid matching
ECDSA/Ed25519 types follow the original crypto path. Existing error normalization
remains (`WS_CRYPTO_FAILED` for key parsing, `WS_INVALID_ALGO_ID` for signature
type mismatch).
- **Local signature-framing correction:** the ECC r/s parser uses the checked
end of the declared signature sub-blob, not the enclosing field size. The
preceding `GetSize` establishes `sz <= signatureSz - i`, so calculating that
end cannot wrap. Both mpints must exactly consume the sub-blob, and the
sub-blob must exactly consume the enclosing signature field. Ed25519 likewise
rejects bytes outside its declared signature string before starting message
verification. These framing errors return `WS_BUFFER_E`. This intentionally
rejects previously tolerated malformed trailing bytes; valid SSH signature
framing and the crypto calls/digest/message construction are unchanged.
This is a local correction verified against the pinned implementation and
[key-validation review](../../docs/ssh_key_validation_review.md), not a claim
that these framing edits came from PR 880.
Reachability evidence: the pinned `DoPacket` dispatches IGNORE, SERVICE_REQUEST,
and CHANNEL_WINDOW_ADJUST to these handlers. `DoUserAuthRequestPublicKey` calls
`DoUserAuthRequestEcc`/`DoUserAuthRequestEd25519` for ECDSA/Ed25519 authentication,
both enabled in this server's reviewed profile. Advertisement is not treated as a parser dispatch filter.
## Explicitly deferred (not fixed by this slice)
- **892:** client `DoServiceAccept`, agent key preparation, daemon authentication,
Windows terminal changes. Password framing/wiping is the existing local
correction, intentionally not replaced with upstream's later formulation.
- **899:** no hunks applied. `ParseRSAPubKey`/`ParseECCPubKey` skips require separate
client/KEX reachability analysis (not the server's `DoUserAuthRequestEcc`).
The old `DoChannelFailure` does not parse a channel ID at all; changing only its
`len != 0` typo would not establish a bounded channel-ID parser. Its existing
behavior is left unchanged rather than claiming the later parser contract.
Windows port/terminal hunks are out of scope.
- **880:** certificate RSA, agent, daemon, terminal, TPM and SCP changes are not
applied. No complete PR-880 closure is claimed.
- Message ordering/state machine (including CVE-2025-14942), service semantics,
standalone ECC curve-name/key-blob semantic validation, other parsers, client
behavior and broader crypto advisories are outside this slice. ECC point/import
validation belongs to the separate crypto-policy owner and is not changed here.
Existing account/key authorization, numeric r/s validity and Ed25519 raw
signature-size/crypto validity checks remain owned by their existing layers.
## Test boundaries
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid
and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary
pairs, unknown channels, ECC equal-length mismatches, shorter/longer matching
prefixes, empty types and every key/signature truncation. Expected ECC and
Ed25519 type bytes end at a protected page, testing unequal-length short-circuit
safety. Crypto and
channel lookup are doubles; tests establish parser gating, not real signature or
point validation. Numeric errors, context/channel layouts and name capacity are
host doubles, not production ABI verification.
The follow-up matrix in `auth_framing.c` covers every truncated ECC sub-blob
boundary with a complete r/s pair still available beyond that boundary, oversized
and wrapping nested lengths, malformed r/s lengths, inner/outer trailing bytes,
and physically guard-page-ended frames. Instrumented `ato32` also asserts that
nested length reads cannot use accessible bytes outside the sub-blob. Signature
input and surrounding canaries stay unchanged. Valid 32-byte and sign-padded
33-byte r/s encodings reach conversion with their bytes/lengths intact. Ed25519
covers both labels, all truncations, shortened/oversized/wrapping/trailing
signature strings, exact raw-signature forwarding and unchanged streamed message
bytes. Both paths retain crypto rejection behavior using doubles.
Validation: **3,124 cases per stack mode** (both pass with UBSan trap mode), plus
**six guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, and Ed25519 exact consumption. The
mutation copies exist only in temporary test files; core dumps are disabled for
those intentionally failing runs. These are framing-valid fixtures with crypto
doubles, not independently verified real signatures.
The runner also compares complete password, packet dispatch, public-key dispatch and selected deferred
functions against the pre-slice generated baseline to fence accidental changes.
The separate auth suite executes its 135 password/control-flow cases, including
payload wipe, callback framing and asynchronous pending retention. No whole-library
fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off
is implied.
@@ -0,0 +1,213 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Included after actual generated parsers and the shared host doubles.
* Signature bytes have valid SSH framing; crypto is intentionally doubled. */
static void check_ecc_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
word32 n, byte *end, int good)
{
byte storage[512], before[512], digest[32]={0};
assert(n+32<=sizeof(storage));
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx};
/* Accessible canaries catch writes; a second run ends at a guard page. */
for (int guarded=0; guarded<2; guarded++) {
memset(storage,0xa5,sizeof(storage));
byte *p=guarded?end-n:storage+16;
memcpy(p,frame,n);
memcpy(before,storage,sizeof(storage));
pk.signature=p; pk.signatureSz=n;
/* Instrument actual length reads as well as crypto calls: accessible
* bytes after a short sub-blob must not even supply the s header. */
word32 inner=8+pk.publicKeyTypeSz, declared=0;
if (n>=inner) {
ato32(frame+inner-4,&declared);
if (declared<=n-inner) {
nested_begin=(uintptr_t)(p+inner);
nested_end=nested_begin+declared;
nested_outer_end=(uintptr_t)(p+n);
}
}
imports=converts=verifies=0;
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
nested_begin=nested_end=nested_outer_end=0;
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
assert(imports==1 && converts==(unsigned)good && verifies==(unsigned)good);
assert(memcmp(storage,before,sizeof(storage))==0);
assert(memcmp(p,frame,n)==0);
cases++;
}
}
static void ecc_framing(byte *end)
{
const byte type[]="ecdsa-sha2-nistp256";
byte key[128]={0}, frame[256], r[33], s[33];
word32 k=string(key,type,sizeof(type)-1);
k+=string(key+k,(const byte*)"nistp256",8);
k+=string(key+k,(const byte*)"Q",1);
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeySz=k,
.publicKeyType=type,.publicKeyTypeSz=sizeof(type)-1};
/* Both ordinary positive mpints and leading-zero sign padding. */
for (word32 rn=32;rn<=33;rn++) for (word32 sn=32;sn<=33;sn++) {
memset(r,0x61,sizeof(r)); memset(s,0x62,sizeof(s));
if (rn==33) { r[0]=0; r[1]=0x80; }
if (sn==33) { s[0]=0; s[1]=0x80; }
memset(frame,0x5a,sizeof(frame));
word32 length_at=string(frame,type,sizeof(type)-1);
word32 inner=length_at+4, blob=8+rn+sn;
put(frame+length_at,blob);
word32 r_at=inner, s_at=inner+4+rn;
string(frame+r_at,r,rn); string(frame+s_at,s,sn);
word32 total=inner+blob;
expected_r=r; expected_s=s; expected_r_sz=rn; expected_s_sz=sn;
check_ecc_frame(pk,frame,total,end,1);
/* All cuts within the nested blob. Backing bytes still contain a
* complete r/s pair: old code consumed beyond the declared boundary. */
for (word32 declared=0;declared<=blob+2;declared++) {
put(frame+length_at,declared);
check_ecc_frame(pk,frame,total,end,declared==blob);
}
const word32 huge[]={UINT32_MAX,UINT32_MAX-inner+1,0x80000000};
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
put(frame+length_at,huge[j]);
check_ecc_frame(pk,frame,total,end,0);
}
put(frame+length_at,blob);
/* Every physical/logical truncation after the valid label, including
* partial nested length headers and both mpints. */
for (word32 cut=length_at;cut<total;cut++)
check_ecc_frame(pk,frame,cut,end,0);
/* A complete pair plus trailing bytes, inside or outside the declared
* sub-blob, must not reach conversion/verification. */
for (word32 extra=1;extra<=8;extra++) {
put(frame+length_at,blob);
check_ecc_frame(pk,frame,total+extra,end,0);
put(frame+length_at,blob+extra);
check_ecc_frame(pk,frame,total+extra,end,0);
}
put(frame+length_at,blob);
for (unsigned which=0;which<2;which++) {
word32 at=which?s_at:r_at, real=which?sn:rn;
word32 wrong[]={0,1,real-1,real+1,blob,UINT32_MAX,0xfffffffc};
for (unsigned j=0;j<sizeof(wrong)/sizeof(*wrong);j++) {
put(frame+at,wrong[j]);
check_ecc_frame(pk,frame,total,end,0);
}
put(frame+at,real);
}
/* A correctly framed signature still propagates crypto rejection. */
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
pk.signature=frame; pk.signatureSz=total;
verify_failure=1; converts=verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,32)==WS_ECC_E);
assert(converts==1 && verifies==1); verify_failure=0; cases++;
}
expected_r=expected_s=NULL;
}
static void reset_ed(void)
{ ed_imports=ed_starts=ed_updates=ed_finals=0; }
static void check_ed_frame(WS_UserAuthData_PublicKey pk, const byte *frame,
word32 n, byte *end, int good)
{
byte storage[512], before[512], data[256], session[32];
memset(data,0x7c,sizeof(data)); memset(session,0x23,sizeof(session));
assert(n+32<=sizeof(storage));
struct context ctx={0};
WOLFSSH ssh={.ctx=&ctx,.sessionId=session,.sessionIdSz=sizeof(session)};
WS_UserAuthData auth={.usernameSz=4,.serviceNameSz=14,.authNameSz=9};
pk.dataToSign=data;
word32 signedSz=4+14+9+1+pk.publicKeyTypeSz+pk.publicKeySz+20;
assert(signedSz<=sizeof(data));
for (int guarded=0;guarded<2;guarded++) {
memset(storage,0xa5,sizeof(storage));
byte *p=guarded?end-n:storage+16;
memcpy(p,frame,n); memcpy(before,storage,sizeof(storage));
pk.signature=p; pk.signatureSz=n;
reset_ed();
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
assert(ret==(good?WS_SUCCESS:WS_BUFFER_E));
assert(ed_imports==1 && ed_starts==(unsigned)good);
assert(ed_updates==(good?4U:0U) && ed_finals==(unsigned)good);
if (good) {
byte expected[512];
put(expected,sizeof(session)); memcpy(expected+4,session,sizeof(session));
expected[4+sizeof(session)]=MSGID_USERAUTH_REQUEST;
memcpy(expected+5+sizeof(session),data,signedSz);
assert(ed_message_sz==5+sizeof(session)+signedSz);
assert(memcmp(ed_message,expected,ed_message_sz)==0);
}
assert(memcmp(storage,before,sizeof(storage))==0);
assert(memcmp(p,frame,n)==0);
cases++;
}
}
static void ed25519_framing(byte *end)
{
const byte type[]="ssh-ed25519";
const word32 typeSz=sizeof(type)-1;
byte key[128], frame[256], rawkey[32], rawsig[64], data[256]={0};
memset(rawkey,0x45,sizeof(rawkey)); memset(rawsig,0x76,sizeof(rawsig));
expected_ed_sig=rawsig; expected_ed_sig_sz=sizeof(rawsig);
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; WS_UserAuthData auth={0};
/* Both label checks, expected type ends at protected memory. */
byte *expected=end-typeSz; memcpy(expected,type,typeSz);
for (unsigned which=0;which<2;which++) for(unsigned mode=0;mode<6;mode++) {
byte bad[40]={0}; memcpy(bad,type,typeSz); word32 n=typeSz;
if(mode==1) bad[0]='X';
if(mode==2) n--;
if(mode==3) n++;
if(mode==4) n=0;
if(mode==5) n=sizeof(bad);
word32 k=string(key,which==0?bad:type,which==0?n:typeSz);
k+=string(key+k,rawkey,sizeof(rawkey));
word32 f=string(frame,which==1?bad:type,which==1?n:typeSz);
f+=string(frame+f,rawsig,sizeof(rawsig));
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=expected,
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
.signatureSz=f,.dataToSign=data};
reset_ed();
int ret=DoUserAuthRequestEd25519(&ssh,&pk,&auth);
if(mode==0) {
assert(ret==0 && ed_imports==1 && ed_starts==1 && ed_updates==4 && ed_finals==1);
}
else {
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
assert(ed_imports==which && ed_starts==0 && ed_updates==0 && ed_finals==0);
}
cases++;
}
word32 k=string(key,type,typeSz); k+=string(key+k,rawkey,sizeof(rawkey));
memset(frame,0x5a,sizeof(frame));
word32 length_at=string(frame,type,typeSz);
word32 total=length_at+string(frame+length_at,rawsig,sizeof(rawsig));
WS_UserAuthData_PublicKey pk={.publicKey=key,.publicKeyType=type,
.signature=frame,.publicKeySz=k,.publicKeyTypeSz=typeSz,
.signatureSz=total,.dataToSign=data};
for (word32 declared=0;declared<=66;declared++) {
put(frame+length_at,declared);
check_ed_frame(pk,frame,total,end,declared==64);
}
const word32 huge[]={UINT32_MAX,UINT32_MAX-length_at,0x80000000};
for (unsigned j=0;j<sizeof(huge)/sizeof(*huge);j++) {
put(frame+length_at,huge[j]); check_ed_frame(pk,frame,total,end,0);
}
put(frame+length_at,64);
for (word32 cut=0;cut<total;cut++)
check_ed_frame(pk,frame,cut,end,0);
for (word32 extra=1;extra<=8;extra++)
check_ed_frame(pk,frame,total+extra,end,0);
/* Key truncations must not import or start signature verification. */
for (word32 cut=0;cut<k;cut++) {
byte *p=end-cut; memcpy(p,key,cut);
pk.publicKey=p; pk.publicKeySz=cut; reset_ed();
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_CRYPTO_FAILED);
assert(ed_imports==0 && ed_starts==0 && ed_finals==0); cases++;
}
pk.publicKey=key; pk.publicKeySz=k; reset_ed(); verify_failure=1;
assert(DoUserAuthRequestEd25519(&ssh,&pk,&auth)==WS_ED25519_E);
assert(ed_starts==1 && ed_updates==4 && ed_finals==1);
verify_failure=0; expected_ed_sig=NULL; cases++;
}
+256
View File
@@ -0,0 +1,256 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Extracted wolfSSH functions retain upstream GPL notices in generated source.
* Crypto doubles test parser gating, not cryptographic validity. */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t byte;
typedef uint32_t word32;
#define WS_SUCCESS 0
#define WS_BUFFER_E -1
#define WS_BAD_ARGUMENT -2
#define WS_INVALID_CHANID -3
#define WS_OVERFLOW_E -4
#define WS_MEMORY_E -5
#define WS_INVALID_ALGO_ID -6
#define WS_CRYPTO_FAILED -7
#define WS_ECC_E -8
#define WS_ED25519_E -9
#define MSGID_USERAUTH_REQUEST 50
#define MSG_ID_SZ 1
#define BOOLEAN_SZ 1
#define Ed25519 0
#define UINT32_SZ 4
#define WOLFSSH_MAX_NAMESZ 32
#define CLIENT_USERAUTH_REQUEST_DONE 42
#define WS_CHANNEL_ID_SELF 0
#define WLOG(...) ((void)0)
#define WOLFSSH_UNUSED(x) ((void)(x))
#define WMEMCPY memcpy
#define WMEMCMP memcmp
#define WMALLOC(n,h,t) malloc(n)
#define WFREE(p,h,t) free(p)
#define ECDSA_ASN_SIG_SZ 80
#define INVALID_DEVID -1
#define WC_SIGNATURE_TYPE_ECC 1
struct context { void *heap; };
typedef struct {
struct context *ctx;
int clientState;
const byte *sessionId;
word32 sessionIdSz;
} WOLFSSH;
typedef struct { word32 peerWindowSz; } WOLFSSH_CHANNEL;
typedef struct {
const byte *publicKey, *publicKeyType, *signature;
word32 publicKeySz, publicKeyTypeSz, signatureSz;
const byte *dataToSign;
} WS_UserAuthData_PublicKey;
typedef struct {
word32 usernameSz, serviceNameSz, authNameSz;
} WS_UserAuthData;
typedef struct { int unused; } ed25519_key;
typedef struct { int unused; } ecc_key;
enum wc_HashType { HASH_SHA256 };
static unsigned imports, converts, verifies, finds, cases;
static unsigned ed_imports, ed_starts, ed_updates, ed_finals;
static const byte *expected_r, *expected_s, *expected_ed_sig;
static word32 expected_r_sz, expected_s_sz, expected_ed_sig_sz;
static byte ed_message[512];
static word32 ed_message_sz;
static int verify_failure;
static uintptr_t nested_begin, nested_end, nested_outer_end;
static WOLFSSH_CHANNEL channel;
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *ssh, word32 id, int side)
{ finds++; return id == 7 ? &channel : NULL; }
static void ato32(const byte *p, word32 *v)
{
uintptr_t address=(uintptr_t)p;
if (nested_begin != 0 && address>=nested_begin && address<nested_outer_end)
assert(address<=nested_end && 4<=nested_end-address);
*v = ((word32)p[0]<<24) | ((word32)p[1]<<16) | ((word32)p[2]<<8) | p[3];
}
static void put(byte *p, word32 v)
{ p[0]=v>>24; p[1]=v>>16; p[2]=v>>8; p[3]=v; }
static int wc_ecc_init_ex(ecc_key *k, void *h, int id) { return 0; }
static int wc_ecc_import_x963(const byte *p, word32 n, ecc_key *k)
{ imports++; return 0; }
static void wc_ecc_free(ecc_key *k) {}
static int wc_ecc_rs_raw_to_sig(const byte *r, word32 rn, const byte *s,
word32 sn, byte *out, word32 *n)
{
converts++;
if (expected_r != NULL) {
assert(rn==expected_r_sz && sn==expected_s_sz);
assert(memcmp(r,expected_r,rn)==0 && memcmp(s,expected_s,sn)==0);
}
return 0;
}
static int wc_SignatureVerifyHash(enum wc_HashType h, int t, byte *d,
word32 dn, byte *s, word32 sn, ecc_key *k, size_t kn)
{ verifies++; return verify_failure; }
static void c32toa(word32 v, byte *p) { put(p,v); }
static int wc_ed25519_init_ex(ed25519_key *key, void *heap, int id) { return 0; }
static void wc_ed25519_free(ed25519_key *key) {}
static int wc_ed25519_import_public(const byte *p, word32 n, ed25519_key *key)
{ ed_imports++; assert(n==32); return 0; }
static int wc_ed25519_verify_msg_init(const byte *sig, word32 n,
ed25519_key *key, byte type, const byte *context, byte contextSz)
{
ed_starts++;
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
ed_message_sz=0;
return 0;
}
static int wc_ed25519_verify_msg_update(const byte *p, word32 n, ed25519_key *key)
{
ed_updates++;
assert(n<=sizeof(ed_message)-ed_message_sz);
if (n != 0)
memcpy(ed_message+ed_message_sz,p,n);
ed_message_sz+=n;
return 0;
}
static int wc_ed25519_verify_msg_final(const byte *sig, word32 n,
int *status, ed25519_key *key)
{
ed_finals++;
assert(n==expected_ed_sig_sz && memcmp(sig,expected_ed_sig,n)==0);
*status=!verify_failure;
return 0;
}
#include "actual.c"
static void parsers(byte *end)
{
struct context ctx = {0}; WOLFSSH ssh = {.ctx=&ctx, .clientState=9};
const word32 lengths[] = {0,1,2,3,4,27,28,31,32,33,255,UINT32_MAX-4,UINT32_MAX};
for (word32 n=0; n<=64; n++) {
byte *p=end-n;
for (unsigned j=0; j<sizeof(lengths)/sizeof(*lengths); j++) {
memset(p, 'x', n);
if (n>=4) put(p,lengths[j]);
word32 idx=0, value=123;
int good=n>=4 && lengths[j]<=n-4;
assert(GetSize(&value,p,n,&idx)==(good?0:WS_BUFFER_E));
idx=0;
assert(DoIgnore(&ssh,p,n,&idx)==(good?0:WS_BUFFER_E));
if (good) assert(idx==4+lengths[j]);
idx=0; ssh.clientState=9;
int service=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==(service?0:WS_BUFFER_E));
assert(ssh.clientState==(service?42:9));
assert(idx==(service?4+lengths[j]:0));
char out[10]; memset(out, 0x55, sizeof(out));
word32 cap=8; idx=0;
assert(GetString(out+1,&cap,p,n,&idx)==(good?0:WS_BUFFER_E));
assert(out[0]==0x55 && out[9]==0x55);
if(good) { assert(cap==(lengths[j]<8?lengths[j]:7)); assert(out[cap+1]==0); }
cap=0; idx=0;
assert(GetString((char*)end,&cap,p,n,&idx)==WS_BUFFER_E);
assert(idx==0);
cases++;
}
word32 invalids[]={n,n+1,UINT32_MAX-3,UINT32_MAX};
for(unsigned j=0;j<4;j++) {
word32 idx=invalids[j], v=0;
assert(GetSize(&v,p,n,&idx)==WS_BUFFER_E);
idx=invalids[j]; assert(DoIgnore(&ssh,p,n,&idx)==WS_BUFFER_E);
idx=invalids[j]; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,n,&idx)==WS_BUFFER_E);
assert(ssh.clientState==9 && idx==invalids[j]); cases++;
}
}
/* Nonzero packet offsets and exact-end empty strings. */
byte p[12]={0}; put(p+3,5); word32 idx=3;
assert(DoIgnore(&ssh,p,12,&idx)==0 && idx==12);
put(p+3,0); idx=3;
assert(DoServiceRequest(&ssh,p,7,&idx)==0 && idx==7);
}
static void windows(byte *end)
{
WOLFSSH ssh={0};
for(word32 n=0;n<8;n++) {
byte *p=end-n; memset(p,0,n); word32 idx=0;
channel.peerWindowSz=123; finds=0;
assert(DoChannelWindowAdjust(&ssh,p,n,&idx)==WS_BUFFER_E);
assert(channel.peerWindowSz==123 && finds==0 && idx==0); cases++;
}
word32 values[]={0,1,2,0x7fffffff,0xfffffffe,UINT32_MAX};
for(unsigned a=0;a<6;a++) for(unsigned b=0;b<6;b++) {
byte p[11]={0}; put(p+3,7); put(p+7,values[b]); word32 idx=3;
channel.peerWindowSz=values[a];
int overflow=values[b]>UINT32_MAX-values[a];
assert(DoChannelWindowAdjust(&ssh,p,11,&idx)==(overflow?WS_OVERFLOW_E:0));
assert(channel.peerWindowSz==(overflow?values[a]:values[a]+values[b]));
assert(idx==11); cases++;
}
byte p[8]={0}; word32 idx=0; channel.peerWindowSz=12;
assert(DoChannelWindowAdjust(&ssh,p,8,&idx)==WS_INVALID_CHANID);
assert(channel.peerWindowSz==12);
}
static word32 string(byte *p, const byte *s, word32 n)
{ put(p,n); memcpy(p+4,s,n); return n+4; }
static void ecc(byte *end)
{
const byte type[]="ecdsa-sha2-nistp256";
const word32 size=sizeof(type)-1;
byte *expected=end-size; memcpy(expected,type,size);
struct context ctx={0}; WOLFSSH ssh={.ctx=&ctx}; byte digest[32]={0};
for(int which=0;which<2;which++) for(int mode=0;mode<6;mode++) {
byte key[128]={0},sig[128]={0}, bad[40]={0};
memcpy(bad,type,size); word32 n=size;
if(mode==1) bad[0]='X'; /* equal length mismatch */
if(mode==2) n--; /* matching prefix, shorter */
if(mode==3) n++; /* expected ends at guard page */
if(mode==4) n=0;
if(mode==5) n=sizeof(bad);
word32 k=string(key,which==0?bad:type,which==0?n:size);
k+=string(key+k,(const byte*)"nistp256",8);
k+=string(key+k,(const byte*)"Q",1);
word32 s=string(sig,which==1?bad:type,which==1?n:size);
put(sig+s,10); s+=4;
s+=string(sig+s,(const byte*)"r",1);
s+=string(sig+s,(const byte*)"s",1);
WS_UserAuthData_PublicKey pk={.publicKey=key, .publicKeyType=expected,
.signature=sig, .publicKeySz=k, .publicKeyTypeSz=size, .signatureSz=s};
imports=converts=verifies=0;
int ret=DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest));
if(mode==0) assert(ret==0 && imports==1 && converts==1 && verifies==1);
else {
assert(ret==(which==0?WS_CRYPTO_FAILED:WS_INVALID_ALGO_ID));
assert(imports==(unsigned)which && converts==0 && verifies==0);
}
cases++;
if (mode==0 && which==0) {
for (word32 cut=0; cut<k; cut++) {
pk.publicKeySz=cut; verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
assert(verifies==0); cases++;
}
pk.publicKeySz=k;
for (word32 cut=0; cut<s; cut++) {
pk.signatureSz=cut; verifies=0;
assert(DoUserAuthRequestEcc(&ssh,&pk,HASH_SHA256,digest,sizeof(digest))!=0);
assert(verifies==0); cases++;
}
}
}
}
#include "auth_framing.c"
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *map=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(map!=MAP_FAILED && mprotect(map+page,page,PROT_NONE)==0);
parsers(map+page); windows(map+page); ecc(map+page);
ecc_framing(map+page); ed25519_framing(map+page);
assert(munmap(map,(size_t)page*2)==0);
printf("PASS: %u parser/window/ECC/Ed25519 cases, guard pages + UBSan trap\n",cases);
return 0;
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Host contracts from freshly generated, original-hash-verified wolfSSH source."""
import os
import resource
from pathlib import Path
import subprocess
import sys
import tempfile
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'tests/wolfssh_auth_contract'))
from run import extract
from security_overrides import ENTRIES, render_entry
entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
assert entry.sha256 == '81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
original, generated = render_entry(entry, {'project': ROOT})
names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef',
'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoUserAuthRequestEd25519')
# Verify this slice cannot accidentally change ordering or existing password logic.
from security_overrides import apply_edits, MODIFICATION_NOTICE, WOLFSSH_PARSER_EDITS
baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), entry.edits[len(WOLFSSH_PARSER_EDITS):])
for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure',
'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'):
assert extract(generated.decode(), name) == extract(baseline, name), name
with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
work = Path(directory)
# Read back the actual generated bytes, not a parallel implementation.
source = work / 'internal.c'
source.write_bytes(generated)
functions = '\n'.join(extract(source.read_text(), n) for n in names)
(work / 'actual.c').write_text(functions)
for small in (False, True):
binary = work / ('contract-small' if small else 'contract')
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-fsanitize=undefined',
'-fsanitize-undefined-trap-on-error',
*(['-DWOLFSSH_SMALL_STACK'] if small else []),
'-I', str(work), str(HERE / 'contract.c'), '-o', str(binary)],
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
subprocess.run([str(binary)], check=True, timeout=30)
# Prove negative fixtures detect removal of each new boundary/type guard.
# Mutations affect only temporary extracted host copies, never the override.
mutations = (
('ECC nested read boundary', 'DoUserAuthRequestEcc',
(('pk->signature, sz, &i)', 'pk->signature, pk->signatureSz, &i)'),)),
('ECC inner exact consumption', 'DoUserAuthRequestEcc',
(('(i != sz || sz != pk->signatureSz)', '(sz != pk->signatureSz)'),)),
('ECC outer exact consumption', 'DoUserAuthRequestEcc',
(('(i != sz || sz != pk->signatureSz)', '(i != sz)'),)),
('Ed25519 key label', 'DoUserAuthRequestEd25519',
(('|| WMEMCMP(publicKeyType,', '&& WMEMCMP(publicKeyType,'),)),
('Ed25519 signature label', 'DoUserAuthRequestEd25519',
(('pk->publicKeyTypeSz ||', 'pk->publicKeyTypeSz &&'),)),
('Ed25519 outer exact consumption', 'DoUserAuthRequestEd25519',
(('if (ret == WS_SUCCESS && sz != pk->signatureSz - i)', 'if (0)'),)),
)
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
for label, name, replacements in mutations:
body = extract(source.read_text(), name)
changed = body
for old, new in replacements:
assert old in changed, label
changed = changed.replace(old, new)
assert changed != body, label
(work / 'actual.c').write_text(functions.replace(body, changed))
binary = work / 'mutation'
subprocess.run(['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-I', str(work),
str(HERE / 'contract.c'), '-o', str(binary)],
check=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'})
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected mutation: {label}'
print(f'PASS: {len(mutations)} parser guard-removal mutations rejected')
print('PASS: exact original hash; generated parser; unchanged ordering/password/deferred functions')
print('NOTE: production build-tree registration/firmware not regenerated or validated')
+222 -1
View File
@@ -119,6 +119,227 @@ TLS_POLICY = """ /* mbedTLS retains this pointer: it must outlive every serve
# DHCP uses equivalent remaining-length checks to avoid forming pointers beyond # DHCP uses equivalent remaining-length checks to avoid forming pointers beyond
# the input object. Keep original upstream notices verbatim, rather than changing # the input object. Keep original upstream notices verbatim, rather than changing
# their copyright year; the central project modification notice is separate. # their copyright year; the central project modification notice is separate.
# Bounded server parser subset of official wolfSSL/wolfssh PRs 892, 881,
# and 880 (reviewed alongside PR 899). Keep the 1.4.20 state machine and
# password/async edits below. GetSize already uses safe remaining lengths.
WOLFSSH_PARSER_EDITS = (
Edit("""int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
int result;
word32 strSz;
result = GetUint32(&strSz, buf, len, idx);
""", """int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
int result;
word32 strSz;
if (*sSz == 0)
return WS_BUFFER_E;
result = GetSize(&strSz, buf, len, idx);
"""),
Edit(""" result = GetUint32(&sz, buf, len, idx);
if (result == WS_SUCCESS) {
result = WS_BUFFER_E;
if (*idx < len && sz <= len - *idx) {""", """ result = GetSize(&sz, buf, len, idx);
if (result == WS_SUCCESS) {
result = WS_BUFFER_E;
if (*idx <= len && sz <= len - *idx) {"""),
Edit(""" word32 dataSz;
word32 begin = *idx;
WOLFSSH_UNUSED(ssh);
WOLFSSH_UNUSED(len);
ato32(buf + begin, &dataSz);
begin += LENGTH_SZ + dataSz;
*idx = begin;
return WS_SUCCESS;""", """ WOLFSSH_UNUSED(ssh);
return GetSkip(buf, len, idx);"""),
Edit(""" WOLFSSH_UNUSED(len);
ato32(buf + begin, &nameSz);
begin += LENGTH_SZ;
if (begin + nameSz > len || nameSz >= WOLFSSH_MAX_NAMESZ) {
return WS_BUFFER_E;
}
WMEMCPY(serviceName, buf + begin, nameSz);
begin += nameSz;
serviceName[nameSz] = 0;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);""", """ int ret = GetSize(&nameSz, buf, len, &begin);
/* Preserve 1.4.20's service-name limit; GetString normally truncates. */
if (ret != WS_SUCCESS || nameSz >= sizeof(serviceName))
return WS_BUFFER_E;
begin = *idx;
nameSz = sizeof(serviceName);
ret = GetString(serviceName, &nameSz, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);"""),
Edit(""" channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
channel->peerWindowSz);""", """ if (bytesToAdd > (word32)0xFFFFFFFFU - channel->peerWindowSz) {
ret = WS_OVERFLOW_E;
}
else {
channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
channel->peerWindowSz);
}"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Public Key's type does not match public key type");""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Public Key's type does not match public key type");"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = GetStringRef(&rSz, &r, pk->signature, pk->signatureSz, &i);"""),
# Local framing correction: GetSize proves i + sz cannot overflow. Bound
# both mpints to that sub-blob, then reject unconsumed inner/outer bytes.
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
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) {
ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz,""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, 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;
if (ret == WS_SUCCESS) {
ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz,"""),
# PR 880's remaining current-feature label checks (Ed25519).
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz
&& WMEMCMP(publicKeyType,
pk->publicKeyType, publicKeyTypeSz) != 0) {""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz
|| WMEMCMP(publicKeyType,
pk->publicKeyType, publicKeyTypeSz) != 0) {"""),
Edit(""" if (publicKeyTypeSz != pk->publicKeyTypeSz &&
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
if (ret == WS_SUCCESS) {
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,""", """ if (publicKeyTypeSz != pk->publicKeyTypeSz ||
WMEMCMP(publicKeyType, pk->publicKeyType, publicKeyTypeSz) != 0) {
WLOG(WS_LOG_DEBUG,
"Signature's type does not match public key type");
ret = WS_INVALID_ALGO_ID;
}
}
if (ret == WS_SUCCESS) {
/* Get the size of the signature blob. */
ret = GetSize(&sz, pk->signature, pk->signatureSz, &i);
}
/* 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,"""),
)
ENTRIES = ( ENTRIES = (
Entry("dhcpserver", "lwip", "idf", Entry("dhcpserver", "lwip", "idf",
"components/lwip/apps/dhcpserver/dhcpserver.c", "components/lwip/apps/dhcpserver/dhcpserver.c",
@@ -206,7 +427,7 @@ ENTRIES = (
), target="mbedx509"), ), target="mbedx509"),
Entry("wolfssh_internal", "wolfssl__wolfssh", "project", Entry("wolfssh_internal", "wolfssl__wolfssh", "project",
"managed_components/wolfssl__wolfssh/src/internal.c", "managed_components/wolfssl__wolfssh/src/internal.c",
"81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", ( "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", WOLFSSH_PARSER_EDITS + (
Edit(""" WS_UserAuthData_Password* pw = NULL; Edit(""" WS_UserAuthData_Password* pw = NULL;
int ret = WS_SUCCESS; int ret = WS_SUCCESS;
""", """ WS_UserAuthData_Password* pw = NULL; """, """ WS_UserAuthData_Password* pw = NULL;