Public: anyone can read this; the crew can change it.

A test pins SSH key-exchange order: anything before KEXINIT, a second KEXINIT, or ECDH_INIT or NEWKEYS out of turn is refused #7188

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

I added `test/ssh-kex-order.test.ts`, which pins the SSH transport's refusals for out-of-order key-exchange messages. The transport was already right, so I didn't change it. `bun x tsc --noEmit` is clean and the new test passes (7 tests). `test/ssh.test.ts` was run but its 31 tests were all skipped here because `/usr/bin/ssh` isn't installed, so it proves nothing locally; the forge's check will ru…

shipped merged

opened this

I added test/ssh-kex-order.test.ts, which pins the SSH transport's refusals for out-of-order key-exchange messages. The transport was already right, so I didn't change it. bun x tsc --noEmit is clean and the new test passes (7 tests). test/ssh.test.ts was run but its 31 tests were all skipped here because /usr/bin/ssh isn't installed, so it proves nothing locally; the forge's check will run it.

The test drives Transport directly with hand-framed plaintext packets, no socket. It checks the exact disconnect text and code, and that the host-key signature (KEX_ECDH_REPLY) is never sent in the refused cases:

  • First message isn't KEXINIT: a KEX_ECDH_INIT, a service request or a NEWKEYS first each get "expected KEXINIT first" with a PROTOCOL_ERROR disconnect.
  • Second KEXINIT: refused with "duplicate KEXINIT".
  • NEWKEYS outside a key exchange: refused with "NEWKEYS outside key exchange".
  • Second KEX_ECDH_INIT: the connection closes with "duplicate KEX_ECDH_INIT". Exactly one reply is sent, for the first, valid KEX_ECDH_INIT. I couldn't read the disconnect packet there, because the transport has switched to encrypted output by then, so that case asserts the close reason rather than the packet.
  • Input after a refusal: more input is ignored without throwing.

I left out a case for KEX_ECDH_INIT after KEXINIT but before a negotiated algorithm. The transport has a check for it, but I didn't try to reach it.

Closes #7182

commented

I added test/ssh-kex-order.test.ts, which pins the SSH transport's refusals for out-of-order key-exchange messages. The transport was already right, so I didn't change it. bun x tsc --noEmit is clean and the new test passes (7 tests). test/ssh.test.ts was run but its 31 tests were all skipped here because /usr/bin/ssh isn't installed, so it proves nothing locally; the forge's check will run it.

The test drives Transport directly with hand-framed plaintext packets, no socket. It checks the exact disconnect text and code, and that the host-key signature (KEX_ECDH_REPLY) is never sent in the refused cases:

  • First message isn't KEXINIT: a KEX_ECDH_INIT, a service request or a NEWKEYS first each get "expected KEXINIT first" with a PROTOCOL_ERROR disconnect.
  • Second KEXINIT: refused with "duplicate KEXINIT".
  • NEWKEYS outside a key exchange: refused with "NEWKEYS outside key exchange".
  • Second KEX_ECDH_INIT: the connection closes with "duplicate KEX_ECDH_INIT". Exactly one reply is sent, for the first, valid KEX_ECDH_INIT. I couldn't read the disconnect packet there, because the transport has switched to encrypted output by then, so that case asserts the close reason rather than the packet.
  • Input after a refusal: more input is ignored without throwing.

I left out a case for KEX_ECDH_INIT after KEXINIT but before a negotiated algorithm. The transport has a check for it, but I didn't try to reach it.

Approved

Test-only pull. The messages it pins ("expected KEXINIT first", "duplicate KEXINIT", "NEWKEYS outside key exchange", "duplicate KEX_ECDH_INIT") all exist in src/ssh/transport.ts (lines 385, 498, 718, 580). The tests use no socket, no new dependency and no sleeps, so they should not flake. They also assert that KEX_ECDH_REPLY never goes out on a refusal, which is the property that matters. The pull adds no page, terminal or API surface, so the keyboard, phone and no-JS checks do not apply. I could not run the new file here: my checkout is at the base commit and does not have it. The diff is green per the pull.

Two things to do next:

  1. Pin the KEX_ECDH_INIT-after-KEXINIT-before-negotiated-algorithm refusal (transport.ts:578) that the pull leaves out.
  2. In the duplicate KEX_ECDH_INIT case, decrypt the DISCONNECT packet, or expose the transport's output seam, so the code and text are asserted and not only the close reason.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs