Reading sshconnect2.c
2,502 lines that take the client from “we have a socket” to “we are authenticated”. This is a guided tour: where the file sits, the one idiom you have to understand before anything else makes sense, and the order to read it in.
Where it sits
Section titled “Where it sits”ssh is one long linear story, and this file is chapters two and three of it.
flowchart TD
A["ssh.c main()"] --> B["ssh_connect()<br/><i>sshconnect.c</i>"]
B --> C["ssh_login()<br/><i>sshconnect.c:1630</i>"]
C --> D["kex_exchange_identification()<br/><i>kex.c</i> — version banners"]
D --> E["ssh_kex2()<br/><i>sshconnect2.c:219</i>"]
E --> F["ssh_userauth2()<br/><i>sshconnect2.c:424</i>"]
F --> G["ssh_session2()<br/><i>ssh.c:2247</i> — channels, shell, forwarding"]
style E fill:#2d6a4f,color:#fff
style F fill:#2d6a4f,color:#fff
So sshconnect2.c owns exactly the middle. The 2 means SSH protocol 2 — a
name left over from when sshconnect1.c sat beside it. Its server-side mirror
is kex.c plus the auth2*.c family; every function here has an opposite
number over there.
It has three parts, and they are wildly unequal:
| Lines | Part | What |
|---|---|---|
| 60–292 | Key exchange | ssh_kex2 builds the algorithm proposal, runs the exchange, and hooks in host-key verification |
| 294–655 | Auth engine | The state machine that picks methods and reacts to the server’s replies |
| 656–2502 | The six methods | Mostly publickey — that one is roughly 700 lines by itself |
The host-key hook
Section titled “The host-key hook”The file’s first function is also the most security-critical thing in it — the point where the client decides whether the machine that answered is the one you meant.
static intverify_host_key_callback(struct sshkey *hostkey, struct ssh *ssh)Nothing in this file calls it. It is installed as a function pointer at
sshconnect2.c:279 into a struct kex vtable slot
declared in kex.h:170, and invoked from inside the key
exchange:
kex_gen_client() kexgen.c — SSH2_MSG_KEX_ECDH_REPLY arrives sshkey_fromb() kexgen.c:168 — deserialise the server's host key kex_verify_host_key() kex.c:1192 — type and curve must match what was negotiated kex->verify_host_key() → this function …then sshkey_verify() proves the server holds the private halfThe body is three checks, each fatal on failure:
| Line | Check | Config |
|---|---|---|
| 95 | sshkey_check_rsa_length — modulus size, plus the hard floor SSH_RSA_MINIMUM_MODULUS_SIZE | RequiredRSASize |
| 98 | key_type_allowed — is this key’s algorithm permitted? | HostKeyAlgorithms |
| 102 | verify_host_key — known_hosts lookup, fingerprint prompt, MITM warning | StrictHostKeyChecking, CheckHostIP |
Four things about it repay attention.
Trust is decided before the signature is checked. At
kexgen.c:170 this runs the instant the host key is
parsed, before sshkey_verify looks at the signature over the exchange hash.
Verifying a signature from a key you were never going to accept is wasted work,
and it means your known_hosts prompt appears first.
The inner verify_host_key() is a different function.
sshconnect.c:1501, 300 lines of known_hosts
handling. This one is only the policy wrapper. The name collision is a
long-standing wart.
It only ever returns 0. Every failure path calls fatal() and exits. The
caller at kex.c:1204 tests for -1 to produce
SSH_ERR_SIGNATURE_INVALID, but for this implementation that branch is dead.
The second argument is unused. ssh is there to satisfy the vtable
signature. What the function actually needs — the hostname as the user typed
it, the resolved struct sockaddr for CheckHostIP and SSHFP lookups, and
the connection info for %-token expansion in KnownHostsCommand — is not in
struct ssh, so the file smuggles it in through statics at
sshconnect2.c:85:
static char *xxx_host;static struct sockaddr_storage xxx_hostaddr;static const struct ssh_conn_info *xxx_conn_info;They are set once in ssh_kex2 and read back here.
The xxx_ prefix is the authors flagging it as exactly the hack it is.
For the argument types themselves — what a struct sshkey holds, which fields
are live on a wire-parsed public key, and who owns it — see
core types.
The one thing to understand first
Section titled “The one thing to understand first”This file is not straight-line code. It is an event dispatch table, and reading it top to bottom expecting control flow will lose you within a page.
The idiom: a function sends a packet and returns. The reply arrives later and lands in a handler that was registered by message number.
ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_SUCCESS, &input_userauth_success);ssh_dispatch_run_fatal(ssh, DISPATCH_BLOCK, &authctxt.success); /* loop until success */That second line (sshconnect2.c:465) is the entire
authentication phase. It reads packets forever, calling whichever handler is
registered for each message number, until authctxt.success flips. Everything
else in the file is either a sender or a handler.
The practical rule: every input_* function is an entry point, not a
subroutine. To find out who “calls” one, do not grep for calls — grep for
ssh_dispatch_set with its name.
A corollary that trips everyone up: message number 60 means three different
things (ssh2.h).
#define SSH2_MSG_USERAUTH_PK_OK 60#define SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ 60#define SSH2_MSG_USERAUTH_INFO_REQUEST 60The 60–79 window is per-method scratch space, re-registered on every method
switch. That is why sshconnect2.c:548 clears the
whole SSH2_MSG_USERAUTH_PER_METHOD_MIN–MAX range before trying a new
method: a stale handler from the previous method would happily misparse the
next one’s packets.
The auth engine, in four functions
Section titled “The auth engine, in four functions”Read these in order and you have the file’s spine.
sequenceDiagram
participant C as ssh
participant S as sshd
C->>S: SERVICE_REQUEST "ssh-userauth"
S->>C: SERVICE_ACCEPT
Note over C: input_userauth_service_accept<br/>registers SUCCESS / FAILURE / BANNER
C->>S: USERAUTH_REQUEST method "none"
S->>C: USERAUTH_FAILURE + methods that can continue
Note over C: input_userauth_failure → userauth()<br/>→ authmethod_get() picks the next
C->>S: USERAUTH_REQUEST next method
S->>C: FAILURE (partial=1) — stage passed, keep going
C->>S: USERAUTH_REQUEST next method
S->>C: USERAUTH_SUCCESS
Note over C: authctxt.success = 1 breaks the dispatch loop
ssh_userauth2— sets upAuthctxt, sendsSERVICE_REQUEST, enters the dispatch loop, and at the end prints theAuthenticated to ... using "%s"line you see under-v.userauth— the driver. Given the server’s list of acceptable methods: pick one, clear the per-method handlers, callmethod->userauth(ssh). If that returns 0 (“I had nothing to send”), disable the method and try the next.input_userauth_failure— the loop’s other half. A failure packet carries a fresh method list and apartialflag, and it callsuserauth()again. This is the actual retry loop — failure → pick next → send → failure. There is noforloop anywhere; it is mutual recursion through the network.authmethod_get— intersectsPreferredAuthentications(your order) with what the server offers (their set), viamatch_list. Your config decides priority; the server only decides membership.
The partial flag at sshconnect2.c:636 is how
multi-factor works: the server says “that one counted, keep going”, and the
client calls pubkey_reset so keys can be offered again in the next stage.
The method table
Section titled “The method table”sshconnect2.c:387 is the index to the back half. Six
rows, five fields each:
{"password", userauth_passwd, NULL, &options.password_authentication, &options.batch_mode},/* name sender cleanup enable flag disable flag */The batch_flag column is small but load-bearing: it is how BatchMode=yes
kills exactly the two methods that would block on a terminal prompt, and
nothing else.
From there each method is self-contained — jump straight to the one you care about:
| Method | Entry point | Note |
|---|---|---|
none | userauth_none | 15 lines. Sent first, always, to harvest the server’s method list |
password | userauth_passwd | Read a passphrase, send it. Plus input_userauth_passwd_changereq for expired-password flows |
keyboard-interactive | userauth_kbdint | Thin; the work is in input_userauth_info_req |
hostbased | userauth_hostbased | Signing needs the host key, which the user cannot read, so it shells out to the setuid ssh-keysign helper via ssh_keysign |
gssapi-with-mic | userauth_gssapi | Only under #ifdef GSSAPI; a multi-packet token exchange with its own handlers |
publickey | userauth_pubkey | See below |
Why publickey is 700 lines
Section titled “Why publickey is 700 lines”Because “sign a challenge” is the easy part. The rest is:
Which keys, in what order.
pubkey_prepare merges four sources — config files,
the agent, PKCS#11, and certificates — into one ordered list. The comment above
it states the policy outright:
- certificates listed in the config file
- agent keys that are found in the config file
- other agent keys
- PKCS#11 keys that are found in the config file
- keys that are only listed in the config file
It then filters twice: against your PubkeyAcceptedAlgorithms, and against the
server’s advertised server-sig-algs. That is where the Skipping ... key
lines under -v come from.
The two-phase probe.
send_pubkey_test offers a public key without
signing anything, and only on PK_OK does
sign_and_send_pubkey do real crypto. This avoids a
security-key touch prompt, a PIN, or an agent round-trip for every candidate
key. It also explains why a failed login shows many Offering public key lines
but few signatures — and why MaxAuthTries counts offers, not signatures.
Per-key algorithm negotiation.
key_sig_algorithm decides whether an RSA key signs
as ssh-rsa, rsa-sha2-256 or rsa-sha2-512, based on the server’s
EXT_INFO. Nearly every “works against the old server, fails against the new
one” bug lands here.
A reading order that works
Section titled “A reading order that works”Start at ssh_login for one page of context, then:
ssh_userauth2 → input_userauth_service_accept → userauth → input_userauth_failure → authmethod_getLoop those five until the state machine clicks. Then follow one simple method
end to end — userauth_passwd is 40 lines — before going near publickey.
The fastest way to make it concrete is to run the client verbosely and read the
trace beside the source. Nearly every debug/debug2/debug3 string in this
file is greppable verbatim, so the trace is an execution log of these
functions in order:
ssh -vvv -o PreferredAuthentications=publickey,password localhost 2>&1 \ | grep -n 'Next authentication\|Offering\|Skipping\|Authentications that can'What is deliberately not here
Section titled “What is deliberately not here”Knowing the boundaries saves a lot of wrong-file searching:
- Host key verification —
ssh_kex2only installs the callback atsshconnect2.c:91. Theknown_hostslogic, the fingerprint prompt and the large MITM warning all live insshconnect.c. - Crypto —
kex.c,kexgen.c,cipher.c,sshkey.c. This file negotiates and calls; it does not compute. - Everything after authentication — channels, port forwarding, the remote
shell:
ssh.candchannels.c. - Agent protocol —
ssh-agent.candauthfd.c. Here it is onlyssh_fetch_identitylistandssh_agent_sign.
For what the six methods actually are, see client authentication. For the structs this file passes around, see core types; for where the whole file sits in a connection, connection workflows. The same protocol from the other end is server-side authentication.