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.
The vocabulary
Section titled “The vocabulary”| Type | Defined in | Role |
|---|---|---|
struct ssh | packet.h:49 | One connection. The handle threaded through nearly every function |
struct sshbuf | sshbuf.c:42 (opaque) | Every byte that goes on or comes off the wire |
struct sshkey | sshkey.h:113 | Every key, of every algorithm, in one struct |
struct kex | kex.h:143 | Key-exchange state — and the vtable that makes client and server share code |
Authctxt | two different structs, same name | Who is authenticating, and how far they have got |
struct Channel | channels.h:129 | One 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 — the connection
Section titled “struct ssh — the connection”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.
struct sshbuf — the wire
Section titled “struct sshbuf — the wire”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 atsize,sshbuf_get_*consumes fromoff. Parsing a packet is just callingget_*in the same order the sender calledput_*. - The encoding is the SSH wire format.
sshbuf_put_cstringwrites a 4-byte big-endian length then the bytes;sshbuf_put_u32writes four bytes. What you see in a hex dump is exactly the struct’s contents fromofftosize. max_sizeis 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_frombmakes a read-only view sharing the parent’s memory with arefcount, 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.
| Family | Live fields |
|---|---|
| RSA, ECDSA | pkey (an OpenSSL EVP_PKEY), plus ecdsa_nid for ECDSA |
| ed25519 | ed25519_pk, and ed25519_sk if private |
| ML-DSA hybrid | mldsa_ed25519_pk / _sk |
FIDO (sk-*) | the base family, plus sk_application, sk_flags, sk_key_handle |
| Certificates | any 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.
struct kex — state plus vtable
Section titled “struct kex — state plus vtable”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 atkex.c:1146and 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.
Authctxt — two structs, one name
Section titled “Authctxt — two structs, one name”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.
| Client | Server | |
|---|---|---|
| Real name | struct cauthctxt | struct Authctxt |
| Defined | sshconnect2.c:314 | auth.h:55 |
| Tracks | which methods remain, the key list to offer, per-method attempt counters | whether the user is valid, failure counts, struct passwd, remaining method lists for multi-factor |
| Key fields | keys (an idlist of struct identity), method, authlist, agent_fd | pw, 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.
struct Channel — the session phase
Section titled “struct Channel — the session phase”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.
Conventions that come with the types
Section titled “Conventions that come with the types”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.
Where the types meet: the host-key hook
Section titled “Where the types meet: the host-key hook”One nine-line function touches four of them at once:
static intverify_host_key_callback(struct sshkey *hostkey, struct ssh *ssh)- Its address is stored in a
struct kexvtable slot, which is why the signature is fixed and cannot take the extra arguments it actually needs. struct sshkey *hostkeyis a public key only — freshly deserialised from the wire bysshkey_fromb, soed25519_skandshielded_privateareNULL. It is borrowed: the caller frees it atkexgen.c:265, unless it transfers ownership tokex->initial_hostkeyfirst. It is non-constonly because the vtable type says so.struct ssh *sshis ignored entirely. The data the function needs — the hostname as the user typed it and the resolvedstruct sockaddr— is not in the connection context, so the file smuggles it through thexxx_-prefixed statics atsshconnect2.c:85.
The full walkthrough, including why it runs before the signature check, is in
reading sshconnect2.c.