Repository navigation
Feat: parse SSH_MSG_EXT_INFO server-sig-algs (RFC 8308) - #1062
Conversation
sshj already advertised ext-info-c to unlock rsa-sha2-* on picky servers, but discarded the server's SSH_MSG_EXT_INFO reply entirely instead of acting on it. Parse it and use the server-sig-algs extension to reorder (never filter/drop) the client's candidate pubkey signature algorithms, so the first publickey auth attempt is more likely to match what the server accepts, cutting a round-trip that today only gets resolved via trial-and-error retries. Addresses the gap identified in #1015. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous version of this test only proved auth still succeeds end-to-end, which it would even without the fix (AcceptAllPublickeyAuthenticator accepts any algorithm eventually). It gave no signal about whether MINA actually sent server-sig-algs or whether sshj acted on it. Now the embedded server's signature factories are restricted to exclude rsa-sha2-512 (sshj's own default top preference for an RSA key), and a Proxy-based spy on the real Transport records the algorithm name of every publickey feeler request in order. Verified this fails deterministically on the pre-fix code (first attempt is rsa-sha2-512, rejected, then retries with rsa-sha2-256) and passes on the fix (first attempt is rsa-sha2-256 directly) - closing the gap identified in review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Follow-up: the original integration test only proved auth still succeeds end-to-end, which it would have even without the fix — Strengthened it in b5aa3fb: the embedded server's signature factories are now restricted to exclude Verified directly:
This closes the gap and gives a decisive, non-tautological end-to-end proof of the feature. |
Unrelated to the server-sig-algs feature in this PR (this file, and the Decoder/Encoder/KeyExchanger loggers it watches, are untouched by that change) - just a pre-existing race surfaced by CI: getLogs() streamed Logback's ListAppender.list (a plain ArrayList) directly, racing against the [2] parameterized case's hot-loop heartbeater thread, which keeps writing/logging immediately after client.close() returns. AppenderBase.doAppend() synchronizes concurrent writers on the appender itself, but the read here never took that same lock. Snapshot the list under it before streaming. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
CI flake unrelated to this PR: `StrictKeyExchangeTest > strictKeyExchange(Config) > [2] ... FAILED` with a `ConcurrentModificationException` inside `getLogs()`. Confirmed unrelated — this branch touches zero files under `src/itest/`, and doesn't touch the `Decoder`/`Encoder`/`KeyExchanger` loggers this test watches (the new EXT_INFO logging goes through `TransportImpl`'s own logger). Root cause: `getLogs()` streamed Logback's `ListAppender.list` (a plain `ArrayList`) directly. Logback's `AppenderBase.doAppend()` is `synchronized` on the appender for concurrent writers, but this read never took that same lock — so it raced against the `[2]` case's hot-loop heartbeater thread (busy-loops with `sleep(0)`), which can still be writing/logging right after `client.close()` returns. Fixed in f986c90 by snapshotting the list under the same lock before streaming. Since it's a pre-existing flake in shared test infra, not part of the feature, happy to split it into its own PR instead if preferred — let me know. |
Summary
Addresses the gap identified in #1015: sshj advertises
ext-info-cin its kex proposal (PR #622) purely to unlockrsa-sha2-*signatures on picky servers, but when the server replies withSSH_MSG_EXT_INFO, the message was logged and discarded entirely — none of its extensions were ever parsed or used.This PR implements the one extension that's practically useful for a client:
server-sig-algs.TransportImpl.gotExtInfo()parsesSSH_MSG_EXT_INFO(uint32 nr-extensions+nr-extensions * (string name, string value)), extractingserver-sig-algsand storing it. Unknown extensions (delay-compression,no-flow-control,elevation, ...) are safely skipped — their value is always consumed so buffer position stays correct for subsequent pairs. Defaults to an empty list until (if ever) received.Transport.getServerSignatureAlgorithms()exposes the parsed list to the userauth layer, following the same pattern as the existinggetHostKeyAlgorithm()/getClientKeyAlgorithms().KeyedAuthMethod.getPublicKeyAlgorithm()uses it to reorder (never filter/drop) the client's own configured candidate algorithms: matches move to the front in the client's own relative order, non-matches remain as fallback. This means:SSH_MSG_USERAUTH_FAILURE.Filtering-to-intersection was considered and rejected: it could remove the client's only remaining fallback if the server's list is incomplete or references an algorithm name this client doesn't map identically, turning a working (if slower) auth into a hard failure. Reordering can only ever match or improve on today's behavior.
Out of scope (noted in code/README):
delay-compression,no-flow-control,elevation, and any client-to-serverEXT_INFOsending.Also updates the README's feature list to document this (RFC 8308 support was previously only implied by a changelog line for #622, which is what prompted #1015's question in the first place).
Test plan
TransportImplExtInfoTest(new): parsesserver-sig-algscorrectly, skips unknown extensions on either side without losing buffer position, defaults to empty before/without receipt, last-wins on repeated extension entries, throwsTransportExceptionon a truncated/malformed message.KeyedAuthMethodTest(new): empty server list is a no-op, a matching subset is moved to front, no-overlap is a no-op, reordering preserves the client's relative preference among matches (not the server's list order), a single configured candidate is always a no-op, and the transport is only queried once per auth attempt (lazy caching preserved).AuthPublickeyExtInfoTest(new): end-to-end RSA publickey auth against a real embedded Apache MINA SSHD server (which sends realserver-sig-algs), asserting successful authentication — a regression guard that the new parse/reorder path doesn't break real auth../gradlew integrationTestlocally; CI'sintegrationjob (which has Docker) will cover that.🤖 Generated with Claude Code