Harden SSH parsing and add notice tooling

- Enforce exact service and channel names with bounded failure parsing
- Add hash-pinned offline notice assembly and regression coverage
- Record advisory dispositions, provenance, integration evidence, and
  remaining gates
This commit is contained in:
2026-09-16 15:06:38 +02:00
parent bea33e1c95
commit 51f835c22f
29 changed files with 3332 additions and 46 deletions
+47 -2
View File
@@ -131,8 +131,9 @@ TLS_POLICY = """ /* mbedTLS retains this pointer: it must outlive every serve
# the input object. Keep original upstream notices verbatim, rather than changing
# 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.
# and 880, plus the current-server PR899/902 disposition documented in
# docs/ssh_parser_remaining_review.md. Preserve ordering/password/async edits.
# GetSize already uses safe remaining lengths.
WOLFSSH_PARSER_EDITS = (
Edit("""int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
@@ -200,9 +201,36 @@ WOLFSSH_PARSER_EDITS = (
ret = GetString(serviceName, &nameSz, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
/* PR902 current-server subset: reject before publishing the transition.
* The owner closes on this error; no best-effort disconnect is queued. */
if (nameSz != sizeof("ssh-userauth") - 1 ||
WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0)
return WS_INVALID_STATE_E;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);"""),
# PR899 fixes the reversed length predicate. The pinned handler additionally
# needs a bounded recipient parser, not merely the later-tree one-line fix.
Edit(""" if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
ret = WS_CHANOPEN_FAILED;""", """ word32 begin, channelId;
if (ssh == NULL || buf == NULL || idx == NULL)
return WS_BAD_ARGUMENT;
begin = *idx;
ret = GetUint32(&channelId, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
if (begin != len)
return WS_BUFFER_E;
if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)
return WS_INVALID_CHANID;
*idx = begin;
ret = WS_CHANOPEN_FAILED;"""),
Edit(""" channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
@@ -351,6 +379,17 @@ WOLFSSH_PARSER_EDITS = (
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,"""),
)
# DoChannelRequest's bounded GetString may truncate at 31 bytes; all recognized
# names are shorter. Exact length first prevents short-name reads and aliases;
# memcmp (not strncmp) also rejects embedded NULs. Keep branch bodies unchanged.
WOLFSSH_PARSER_EDITS += tuple(
Edit(f'WSTRNCMP(type, "{name}", typeSz) == 0',
f'typeSz == sizeof("{name}") - 1 &&\n'
f' WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0')
for name in ("env", "shell", "exec", "subsystem", "pty-req", "window-change",
"exit-status", "exit-signal", "auth-agent-req@openssh.com")
)
ENTRIES = (
Entry("dhcpserver", "lwip", "idf",
"components/lwip/apps/dhcpserver/dhcpserver.c",
@@ -649,6 +688,12 @@ def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]:
" * plus project restricted no-EXT_INFO correction. Provenance and\n"
" * limitations: tools/wolfssh_order/README.md and delta.json.\n"
" */\n")
if entry.name == "wolfssh_internal":
notice += ("/* Server parser review modified 2026-09-16: bounded CHANNEL_FAILURE\n"
" * and ssh-userauth service validation; PR899/902 subset, not full PRs.\n"
" * Local follow-up: exact bounded channel-request names.\n"
" * Provenance/limits: docs/ssh_parser_remaining_review.md.\n"
" */\n")
if entry.header:
notice += ("#if defined(_WOLFSSH_INTERNAL_H_) && \\\n"
" (!defined(SAK_WOLFSSH_ORDER_ABI) || SAK_WOLFSSH_ORDER_ABI != 20260916)\n"