Skip to content

Core types

OpenSSH is plain C with a small, consistent object vocabulary. Six types carry almost everything, and once you can name them most files in the tree stop looking foreign. This page is what each one is, who owns it, and where it sits in a live connection.

TypeDefined inRole
struct sshpacket.h:49One connection. The handle threaded through nearly every function
struct sshbufsshbuf.c:42 (opaque)Every byte that goes on or comes off the wire
struct sshkeysshkey.h:113Every key, of every algorithm, in one struct
struct kexkex.h:143Key-exchange state — and the vtable that makes client and server share code
Authctxttwo different structs, same nameWho is authenticating, and how far they have got
struct Channelchannels.h:129One multiplexed stream inside the connection, after authentication

They nest roughly like this:

flowchart TD
    SSH["struct ssh<br/><i>the connection</i>"]
    SSH --> ST["session_state<br/><i>opaque: fds, ciphers, seqnrs</i>"]
    SSH --> KEX["struct kex"]
    SSH --> DISP["dispatch[DISPATCH_MAX]<br/><i>msg number → handler</i>"]
    SSH --> AC["void *authctxt<br/><i>Authctxt, either flavour</i>"]
    SSH --> CH["ssh_channels<br/><i>→ struct Channel[]</i>"]
    KEX --> HK["struct sshkey<br/><i>initial_hostkey</i>"]
    KEX --> SID["struct sshbuf<br/><i>session_id, my, peer, …</i>"]
    CH --> CB["struct sshbuf<br/><i>input / output / extended</i>"]
    style SSH fill:#2d6a4f,color:#fff
struct ssh {
struct session_state *state; /* opaque: fds, buffers, ciphers, seqnrs */
struct kex *kex;
char *remote_ipaddr; int remote_port;
char *local_ipaddr; int local_port;
dispatch_fn *dispatch[DISPATCH_MAX]; /* message number → handler */
uint32_t compat; /* per-peer bug workaround flags */
TAILQ_HEAD(, key_entry) private_keys, public_keys;
void *authctxt;
struct ssh_channels *chanctxt;
void *app_data;
};

Two design decisions worth noticing:

state is deliberately opaque. packet.h:40 only forward-declares struct session_state; the definition lives at packet.c:112. Everything touching sockets, cipher contexts and sequence numbers has to go through the ssh_packet_* API. This is the one hard encapsulation boundary in the codebase.

authctxt and app_data are void *. That is what lets a single packet layer serve ssh, sshd-session, ssh-keyscan and the regression harness without any of them knowing about the others.

The dispatch[] array is the heart of the protocol machinery: handlers are registered by message number and the read loop calls whatever is installed. See reading sshconnect2.c for what that does to control flow.

Every protocol message is built into, or parsed out of, one of these. The struct is private to sshbuf.c:

struct sshbuf {
u_char *d; /* data */
const u_char *cd; /* const alias of d, used when readonly */
size_t off; /* first unread byte is d + off */
size_t size; /* last valid byte is d + size - 1 */
size_t max_size; /* refuse to grow past this */
size_t alloc;
int readonly; /* wraps someone else's const memory */
u_int refcount;
struct sshbuf *parent; /* set if this is a child view of another buffer */
};

Four properties that explain most of its API:

  • It is a queue, not an array. sshbuf_put_* appends at size, sshbuf_get_* consumes from off. Parsing a packet is just calling get_* in the same order the sender called put_*.
  • The encoding is the SSH wire format. sshbuf_put_cstring writes a 4-byte big-endian length then the bytes; sshbuf_put_u32 writes four bytes. What you see in a hex dump is exactly the struct’s contents from off to size.
  • max_size is a security control. Attacker-supplied length fields are parsed against a bounded buffer, so a hostile “this string is 4 GB” cannot turn into a 4 GB allocation.
  • Child buffers borrow. sshbuf_fromb makes a read-only view sharing the parent’s memory with a refcount, so nested structures (a certificate inside a key blob inside a packet) parse without copying.

struct sshkey — every algorithm, one struct

Section titled “struct sshkey — every algorithm, one struct”
struct sshkey {
int type; /* the tag: KEY_RSA, KEY_ED25519, KEY_ECDSA, … */
int flags;
int ecdsa_nid; /* ECDSA: which curve */
EVP_PKEY *pkey; /* libcrypto-backed: RSA, ECDSA */
u_char *ed25519_sk, *ed25519_pk; /* ed25519: raw bytes */
u_char *mldsa_ed25519_sk, *mldsa_ed25519_pk; /* ML-DSA hybrid */
char *sk_application; /* FIDO/U2F security keys */
uint8_t sk_flags;
struct sshbuf *sk_key_handle, *sk_reserved;
struct sshkey_cert *cert; /* non-NULL iff a certificate */
u_char *shielded_private; /* in-memory key shielding */
size_t shielded_len;
u_char *shield_prekey;
size_t shield_prekey_len;
};

It is a tagged struct, not a union. type — from enum sshkey_types at sshkey.h:53 — says which fields are live; the rest are NULL. Every instance carries every field, so a 256-bit ed25519 key has the same footprint as an RSA one. That is the price of “one type, dispatch on type”, a pattern you meet all over this codebase.

FamilyLive fields
RSA, ECDSApkey (an OpenSSL EVP_PKEY), plus ecdsa_nid for ECDSA
ed25519ed25519_pk, and ed25519_sk if private
ML-DSA hybridmldsa_ed25519_pk / _sk
FIDO (sk-*)the base family, plus sk_application, sk_flags, sk_key_handle
Certificatesany of the above, plus cert

Note that public and private keys are the same type. A key parsed off the wire has all the private-side fields NULL; whether you may sign with it is a runtime property, not a type-level one. shielded_private is a hardening measure — private key bytes are kept encrypted under a prekey while idle, so a memory disclosure bug does not hand over the key.

kex.h:143 holds what was negotiated (name, hostkey_alg, kex_type, hash_alg), the buffers that feed the exchange hash (my, peer, client_version, server_version), and per-algorithm scratch space for DH, ECDH and the post-quantum KEMs.

The interesting part is the function pointers:

int (*verify_host_key)(struct sshkey *, struct ssh *);
struct sshkey *(*load_host_public_key)(int, int, struct ssh *);
struct sshkey *(*load_host_private_key)(int, int, struct ssh *);
int (*sign)(struct ssh *, struct sshkey *, struct sshkey *, …);
int (*kex[KEX_MAX])(struct ssh *);

This is how one key-exchange implementation serves both ends. kexgen.c does not branch on client-versus-server; it calls kex->verify_host_key(...) and gets the client’s known_hosts check or the server’s equivalent depending on who filled the slot. The client fills it at sshconnect2.c:279; the kex[] array is filled with kex_gen_client on the client and kex_gen_server on the server.

Two fields outlive the exchange itself:

  • session_id — the very first exchange hash, set once at kex.c:1146 and never replaced, even across rekeys. It is the connection’s identity: signatures during authentication are computed over it, which is what binds an authentication to this connection and stops a captured signature being replayed elsewhere.
  • initial_hostkey / initial_sig — kept so a rekey can be checked against the host key you originally accepted.

This one genuinely catches people. There are two unrelated types with the same typedef name, and which one ssh->authctxt points at depends on which binary you are in.

ClientServer
Real namestruct cauthctxtstruct Authctxt
Definedsshconnect2.c:314auth.h:55
Trackswhich methods remain, the key list to offer, per-method attempt counterswhether the user is valid, failure counts, struct passwd, remaining method lists for multi-factor
Key fieldskeys (an idlist of struct identity), method, authlist, agent_fdpw, valid, postponed, auth_methods, kbdintctxt

The void *authctxt in struct ssh is exactly the seam that lets both live in the same packet layer. Grep results for “Authctxt” therefore span two different things — always check which file you are in.

The client’s struct identity is worth knowing separately: one candidate key, whether it came from the agent (agent_fd), whether the private half is loaded (isprivate), and whether it has been offered yet (tried). The ordered list of them is what pubkey_prepare builds.

After authentication the connection becomes a multiplexer, and every stream — your shell, each port forward, agent forwarding, X11, the mux control socket — is a struct Channel (channels.h:129).

The shape is a small state machine bolted to some file descriptors:

int type; /* SSH_CHANNEL_OPEN, _LARVAL, _CONNECTING, … */
int self; /* our channel id */
uint32_t remote_id; /* peer's id for the same channel */
u_int istate, ostate; /* receive-half and transmit-half states */
int rfd, wfd, efd, sock;
struct sshbuf *input; /* read from the fd, waiting to be encrypted out */
struct sshbuf *output; /* arrived encrypted, waiting to be written to the fd */
struct sshbuf *extended; /* stderr */

type values are at channels.h:42 and name the whole feature set: SSH_CHANNEL_PORT_LISTENER, SSH_CHANNEL_AUTH_SOCKET, SSH_CHANNEL_DYNAMIC (SOCKS), SSH_CHANNEL_MUX_LISTENER. Reading that list is a fast way to learn what the session phase can actually do.

The two-buffer design is the flow-control story: bytes move fd → input → network and network → output → fd, and the SSH window mechanism throttles based on how full they are.

Error handling is int return codes. Almost every function returns 0 or a negative SSH_ERR_* (ssherr.h), rendered by ssh_err(). The house style is if ((r = f(...)) != 0) goto out; with a single cleanup label — you will see this thousands of times.

Allocation does not fail. xmalloc/xcalloc/xstrdup call fatal() on exhaustion, so there is no null check after them. Anything with a plain malloc is deliberate.

Ownership is borrow-by-default. A function receiving a pointer does not free it unless it says so. Transfers are explicit and usually visible as the caller nulling its own pointer afterwards — as at kexgen.c:252.

One nine-line function touches four of them at once:

static int
verify_host_key_callback(struct sshkey *hostkey, struct ssh *ssh)
  • Its address is stored in a struct kex vtable slot, which is why the signature is fixed and cannot take the extra arguments it actually needs.
  • struct sshkey *hostkey is a public key only — freshly deserialised from the wire by sshkey_fromb, so ed25519_sk and shielded_private are NULL. It is borrowed: the caller frees it at kexgen.c:265, unless it transfers ownership to kex->initial_hostkey first. It is non-const only because the vtable type says so.
  • struct ssh *ssh is ignored entirely. The data the function needs — the hostname as the user typed it and the resolved struct sockaddr — is not in the connection context, so the file smuggles it through the xxx_-prefixed statics at sshconnect2.c:85.

The full walkthrough, including why it runs before the signature check, is in reading sshconnect2.c.