Skip to content

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.

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:

LinesPartWhat
60–292Key exchangessh_kex2 builds the algorithm proposal, runs the exchange, and hooks in host-key verification
294–655Auth engineThe state machine that picks methods and reacts to the server’s replies
656–2502The six methodsMostly publickey — that one is roughly 700 lines by itself

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 int
verify_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 half

The body is three checks, each fatal on failure:

LineCheckConfig
95sshkey_check_rsa_length — modulus size, plus the hard floor SSH_RSA_MINIMUM_MODULUS_SIZERequiredRSASize
98key_type_allowed — is this key’s algorithm permitted?HostKeyAlgorithms
102verify_host_keyknown_hosts lookup, fingerprint prompt, MITM warningStrictHostKeyChecking, 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.

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.

sshconnect2.c:516
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 60

The 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_MINMAX range before trying a new method: a stale handler from the previous method would happily misparse the next one’s packets.

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
  1. ssh_userauth2 — sets up Authctxt, sends SERVICE_REQUEST, enters the dispatch loop, and at the end prints the Authenticated to ... using "%s" line you see under -v.
  2. userauth — the driver. Given the server’s list of acceptable methods: pick one, clear the per-method handlers, call method->userauth(ssh). If that returns 0 (“I had nothing to send”), disable the method and try the next.
  3. input_userauth_failure — the loop’s other half. A failure packet carries a fresh method list and a partial flag, and it calls userauth() again. This is the actual retry loop — failure → pick next → send → failure. There is no for loop anywhere; it is mutual recursion through the network.
  4. authmethod_get — intersects PreferredAuthentications (your order) with what the server offers (their set), via match_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.

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:

MethodEntry pointNote
noneuserauth_none15 lines. Sent first, always, to harvest the server’s method list
passworduserauth_passwdRead a passphrase, send it. Plus input_userauth_passwd_changereq for expired-password flows
keyboard-interactiveuserauth_kbdintThin; the work is in input_userauth_info_req
hostbaseduserauth_hostbasedSigning needs the host key, which the user cannot read, so it shells out to the setuid ssh-keysign helper via ssh_keysign
gssapi-with-micuserauth_gssapiOnly under #ifdef GSSAPI; a multi-packet token exchange with its own handlers
publickeyuserauth_pubkeySee below

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:

  1. certificates listed in the config file
  2. agent keys that are found in the config file
  3. other agent keys
  4. PKCS#11 keys that are found in the config file
  5. 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.

Start at ssh_login for one page of context, then:

ssh_userauth2 → input_userauth_service_accept → userauth
→ input_userauth_failure → authmethod_get

Loop 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:

Terminal window
ssh -vvv -o PreferredAuthentications=publickey,password localhost 2>&1 \
| grep -n 'Next authentication\|Offering\|Skipping\|Authentications that can'

Knowing the boundaries saves a lot of wrong-file searching:

  • Host key verificationssh_kex2 only installs the callback at sshconnect2.c:91. The known_hosts logic, the fingerprint prompt and the large MITM warning all live in sshconnect.c.
  • Cryptokex.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.c and channels.c.
  • Agent protocolssh-agent.c and authfd.c. Here it is only ssh_fetch_identitylist and ssh_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.