Chapter Orientation
After KEX the connection is encrypted, but you haven't proven identity yet. SSH authentication (RFC 4252) takes the shape of a method menu: the client tries one by one the methods the server allows — password (username and passphrase), publickey (private-key signature — the signature-free probe is its gem), keyboard-interactive (multi-round challenges — the carrier of OTP/PAM), hostbased (host vouching). xssh's layering: method modules (four contract cards for password/publickey/keyboard/hostkey — each manages only its own payload's building and parsing, storing no credentials and managing no rounds) atop the common message layer (ssh_auth_message — encoding/decoding of USERAUTH_REQUEST/FAILURE/SUCCESS/BANNER); session orchestration (ssh_auth_session — service request, stage validation, budgets, direction consistency) stitches them to the transport core (Chapter 121); the budget layer (ssh_auth_guard — pure resource budgets, shared by client and server) limits attempts/rounds/bytes/time. Unknown method messages (60..79) pass through uniformly — a new authentication method composes the existing session directly, touching no state machine.
126.1引入
The publickey method's probe mechanism deserves the opening: the client first sends a signature-free "do you accept this key?" inquiry (AuthPublicKeyWrite) — the server answers PK_OK (this key accepted) or FAILURE; only after confirmation does the client send the signed request (saving one expensive signature and avoiding leaking signature material to servers that don't support it). The signature material is generated by AuthPublicKeySignDataWrite as string(session_id) || USERAUTH_REQUEST — the contiguous result goes to the caller's signer: hardware keys, ssh-agent, XRT cryptography, or a custom implementation. Another instance of Chapter 83's "deterministic core + external signer" pattern.
keyboard-interactive's multi-round shape is the second attraction: the server sends challenges (a prompt list — "password?" "OTP digit 3?"), the client answers, the server may challenge again — the protocol carrier of multi-factor authentication. Constraints: at most one unfinished challenge at a time; total attempts/rounds/bytes/deadline are limited by the budget layer — "anti-brute-force" is a budget, not luck.
126.2概念
Session orchestration: layering and transactions
Responsibilities split four ways: method modules own payloads, the session owns stages and budgets, the core owns framing/encryption/ordering, the driver owns the network — no layer crosses its border. banner (the server's welcome/warning text) changes no main event; failure permits a new request (the method menu's "next"); success can be committed only by the server direction (direction consistency — a client cannot "proclaim itself successful"). During rekey the driver routes transport control messages (KEXINIT/NEWKEYS/DISCONNECT) to the KEX/transport state machines first and afterwards continues the same auth session — authentication survives rekey.
The four method cards
- password (
ssh_auth_password): USERAUTH_REQUEST + plaintext passphrase (the connection is already encrypted — contrast Chapter 106's Basic: inside an encrypted tunnel this is acceptable);PasswordWrite/Readbuilds and parses directly. - publickey: probe (signature-free) → PK_OK → signed (signature material SignDataWrite → external signer → signature into the request); the public-key blob's algorithm may differ from the request's (an RSA SHA-2 request with an
ssh-rsablob — a real-world compatibility detail of the protocol), but the signature blob's algorithm must match the request's. - keyboard-interactive (RFC 4256): request (empty language canonical advised) → challenge (name/instruction/prompt list) → response; a "full pre-validation + borrowed iteration" interface (prompts non-empty UTF-8, responses may be empty; counts set by protocol fields and the caller's buffers, no fixed in-library ceiling, no object arrays allocated); multiple rounds guaranteed single-challenge by the state machine + total rounds bounded by the budget.
- hostbased (
ssh_auth_hostbased): the signature of the client host's key + hostname vouching — machine-to-machine trust in cluster environments; the format is publickey's family.
The budget layer: auth guard
ssh_auth_guard is a pure resource-budget layer — it parses no messages and stores no usernames: attempt count (USERAUTH_REQUEST counting), interactive rounds (server method messages count rounds, client responses count messages), byte budget, deadline. Shared by both ends: the server defends against brute force (limiting the client's attempts), the client defends against a dragging server (limiting how long it can be strung along) — the budget is a two-way defense.
View discipline (yet again)
The Request/Failure/Banner/Method views borrow the packet and are valid only before the read commits — asynchronous authentication backends (asking for an OTP takes time) must copy any fields kept across the wait. The same discipline as Chapters 96/104/110 — the SSH layer is its fourth appearance; you have met it in every network layer.
126.3示例
First complete program: building the publickey messages
The program below is from examples/auth_publickey — the probe and public-key formats combined:
#include <stdio.h> #include <xssh.h> /* 构建 Ed25519 publickey 探测并读取算法。 */ int main(void) { unsigned char arrRawKey[XSSH_ED25519_PUBLIC_SIZE] = { 0u }; unsigned char arrKey[64]; unsigned char arrPayload[160]; xsshwriter KeyWriter; xsshwriter Writer; xsshauthpublickey PublicKey; if ( !xrtSshWriterInit(&KeyWriter, arrKey, sizeof(arrKey)) || (xrtSshEd25519PublicKeyWrite( &KeyWriter, (xbytesview){ arrRawKey, sizeof(arrRawKey) } ) != XSSH_OK) || !xrtSshWriterInit( &Writer, arrPayload, sizeof(arrPayload) ) || (xrtSshAuthPublicKeyWrite( &Writer, XRT_STR_LITERAL("alice"), XRT_STR_LITERAL(XSSH_HOSTKEY_ED25519), (xbytesview){ arrKey, KeyWriter.Size } ) != XSSH_OK) || (xrtSshAuthPublicKeyRead( (xbytesview){ arrPayload, Writer.Size }, &PublicKey ) != XSSH_OK) ) { return 1; } printf("algorithm=%.*s bytes=%zu\n", (int)PublicKey.Algorithm.Size, PublicKey.Algorithm.Data, Writer.Size); return 0; }
What just happened. (1) The public-key blob produced by Chapter 123's hostkey sample becomes authentication material here — one encoding runs through the handshake (K_S), storage (known_hosts), and authentication (publickey blob), xssh's format-layer reuse. (2) xrtSshAuthPublicKeyWrite builds the signature-free probe — the printed algorithm and bytes are the "may I use this key?" inquiry message. (3) Every step of the real sequence (probe→PK_OK→SignData→signer→signed request) is an independent entrance — the external signer slots in between SignData and signed (the HSM/agent socket). Companions: auth_password (passphrase messages), auth_keyboard (the challenge/response iteration interface), auth_hostbased (the host-vouching form), auth_message (the common layer), auth_guard (the four budget lines).
Second complete program: the session start and service request
The second program is from examples/auth_session — the orchestration layer's starting point:
#include <stdio.h> #include <xssh.h> /* 展示认证编排对象与直接写入最终 payload 的 service 起点。 */ int main(void) { unsigned char arrPayload[64]; xsshauthsession Session; xsshwriter Writer; if ( !xrtSshAuthSessionInit(&Session, XSSH_ROLE_CLIENT) || !xrtSshWriterInit(&Writer, arrPayload, sizeof(arrPayload)) || (xrtSshServiceRequestWrite( &Writer, XRT_STR_LITERAL(XSSH_SERVICE_USERAUTH) ) != XSSH_OK) ) { return 1; } printf( "session=%zu service-request=%zu event=%d\n", sizeof(Session), Writer.Size, (int)xrtSshAuthSessionEvent(&Session) ); return 0; }
What just happened. (1) xrtSshAuthSessionInit(&Session, XSSH_ROLE_CLIENT) — a session on the stack; the sizeof output (the same zero-burden statement as Chapters 118/119). (2) xrtSshServiceRequestWrite writes the ssh-userauth service request — authentication's first message (declare which service before discussing methods); Writer.Size is the message size. (3) xrtSshAuthSessionEvent queries the current main event — the session is event-driven (what to send/wait for now); the driver layer dispatches method modules by event — Chapter 127's runtime is exactly the complete form of that dispatch. (4) Begin's precondition: it accepts only a same-role transport that has completed the first KEX round — layering order enforced (no authentication without encryption).
126.4契约
- Responsibilities split four ways: methods own payloads, the session owns stages + budgets, the core owns framing, the driver owns the network — none crosses.
- Write transaction: the method writes the payload → SessionPrepare (stage + budget) → core wraps → network takes over → double commit; to cancel, drop the core first, then Abort (the budget is not consumed).
- Read transaction: the core validates → SessionPrepare (views lent out) → accept double-commits; on rejection ReadAbort + close the transport.
- View discipline: Request/Failure/Banner/Method borrow the packet, valid before the read commits — async backends copy what they need.
- banner/failure/success: banner changes no main event; failure permits new requests; success commits in the server direction only.
- publickey: the probe is signature-free (sign only after PK_OK); the signature material
session_id||REQUESTgoes to the external signer; the blob algorithm may differ from the request's, the signature algorithm must match. - keyboard: a single unfinished challenge; prompts non-empty UTF-8 / responses may be empty; no in-library ceiling, no arrays allocated; multiple rounds managed by state machine + budget.
- Unknown methods (60..79): pass-through as
PACKET_METHOD+ Method lent out — new methods compose the existing session. - Budgets both ways: attempts/rounds/bytes/deadline, four lines; the server against brute force, the client against dragging.
- rekey coexistence: control messages route to KEX/transport; afterwards the same auth session continues.
Common Mistakes
Symptom: every authentication performs an expensive signature — HSM latency amplified; and servers that don't accept the key still receive signature material (a signature-oracle risk surface).
Cause: the probe's design is "ask first, sign later" — only PK_OK is worth signing. Skipping it saves one round trip and pays signature cost plus a security surface.
/* every time, a signed request straight away */ sign_and_send(Signer, Key); /* signed even though the server will reject */
xrtSshAuthPublicKeyWrite(&Writer, Key, ...); /* probe: signature-free */ send_and_wait(); if ( got_pk_ok() ) { xrtSshAuthPublicKeySignDataWrite(&Sig, ...); /* material */ Signature = signer_sign(Signer, &Sig); /* external signer */ xrtSshAuthPublicKeySignedWrite(&Writer, Key, Signature, ...); }
Symptom: in multi-round authentication, the second round reads garbage prompts — the first round's views died with the released packet.
Cause: challenge/response views borrow the packet; keeping across rounds (across waits) requires copying. Asynchronous OTP backends are the high-incidence zone.
on_challenge(Prompt) { g_Prompts = Prompt; /* stored views: next round's packet has changed */ }
on_challenge(xsshauthkeyboard* pAuth) { copy_prompts(pCh, &g_State); /* copy everything that crosses a wait */ }
Symptom: connected to a malicious server, authentication is forever "one more round" — the client is strung along in endless challenges.
Cause: budgets are not a server-only need — the client's deadline and round ceiling are self-protection.
/* the client sets no guard - the server gets as many rounds as it asks for */xsshauthguardpolicy Policy; xrtSshAuthGuardPolicyInit(&Policy); /* RFC-recommended defaults */ Policy.MaxAttempts = 3; /* at most 3 attempts */ Policy.MaxRounds = 5; /* at most 5 interactive rounds */ xrtSshAuthGuardInit(&Guard, &Policy, DeadlineMs); /* disconnect proactively over the limit - stop-loss against dragging attacks */
Programming Exercises
Run the auth_password/publickey/keyboard/hostbased samples, annotating each sample's message type and fields. Acceptance criteria: you can draw USERAUTH_REQUEST's method-field differences (password plaintext / publickey blob + signature / keyboard submethods).
Drive deterministically: probe→simulated PK_OK→SignDataWrite→fake signer (fixed signature)→SignedWrite→simulated SUCCESS. Count the budget throughout. Acceptance criteria: each step's message parses back; before SUCCESS the budget consumed is exactly two requests.
keyboard over three rounds: passphrase→OTP→new-passphrase confirmation (simulating PAM); client budget (3 rounds, 30 seconds); server budget (anti-brute-force). Both the correct and the wrong OTP paths. Acceptance criteria: the correct path passes SUCCESS in three rounds; the wrong path gets FAILURE with the budget correctly deducted; the client-timeout path disconnects proactively.
Summary
| Topic | Quick reference |
|---|---|
| Layering | methods (payload) → common message layer → session orchestration (stage + budget) → transport core → network |
| Write transaction | method writes → SessionPrepare → core wraps → takeover → double commit; cancellation consumes no budget |
| Read transaction | core validates → SessionPrepare (views lent) → double commit / Abort + close |
| Four methods | password (plaintext inside the tunnel) / publickey (probe + external signer) / keyboard (multi-round challenges) / hostbased |
| probe | signature-free inquiry → sign only after PK_OK; signature material session_id‖REQUEST |
| Algorithm details | the blob algorithm may differ from the request's (RSA SHA-2 with ssh-rsa blob); the signature algorithm must match |
| keyboard | a single unfinished challenge; prompts non-empty / responses may be empty; no ceiling, no allocation; budget bounds rounds |
| Unknown methods | 60..79 pass-through + Method lent out — new methods touch no state machine |
| Budgets both ways | attempts/rounds/bytes/deadline; anti-brute-force (server) + anti-dragging (client) |
| View discipline | borrowing the packet, valid before commit — async backends copy |
📖 API references in this chapter:xssh-ssh_auth_session (18 entries)xssh-ssh_auth_publickey (6 entries)xssh-ssh_auth_keyboard (9 entries)
Click for full signatures, parameter constraints, return values and error notes