ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_server.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_server.cpp
6 * @brief SSH message dispatcher implementation.
7 */
8
15#include "server/mmgr/scratch.h"
16#if PC_ENABLE_SSH_ZLIB
18#endif
19#include <string.h>
20
21// All SSH server-layer state, owned by one instance (internal linkage): the packet-emit
22// callback. One named owner, unreachable from any other translation unit.
24{
25 SshEmitCb emit_cb = nullptr;
26};
27static SshServerCtx s_srv;
28
30{
31 s_srv.emit_cb = cb;
32}
33
34static inline void emit(uint8_t i, const uint8_t *p, size_t n)
35{
36 if (s_srv.emit_cb && n > 0)
37 {
38 s_srv.emit_cb(i, p, n);
39 }
40}
41
42// Build and emit SSH_MSG_DISCONNECT("too many authentication failures") - the reason code, the
43// description string, and an empty language tag (RFC 4253 §11.1) - then the caller closes.
44static void emit_auth_failure_disconnect(uint8_t i, pc_span out)
45{
46 static const char desc[] = "too many authentication failures";
48 const uint32_t dl = (uint32_t)(sizeof(desc) - 1);
49 const size_t need = 13 + dl; // 1 msg + 4 reason + 4 desc length + desc + 4 language tag
50 if (out.cap < need)
51 {
52 return; // fail closed: no room for the whole message, so emit none of it
53 }
54 size_t o = 0;
55 out.buf[o++] = SSH_MSG_DISCONNECT;
56 out.buf[o++] = (uint8_t)(reason >> 24); // reason code (uint32, big-endian)
57 out.buf[o++] = (uint8_t)(reason >> 16);
58 out.buf[o++] = (uint8_t)(reason >> 8);
59 out.buf[o++] = (uint8_t)(reason);
60 out.buf[o++] = (uint8_t)(dl >> 24); // description (uint32 length + bytes)
61 out.buf[o++] = (uint8_t)(dl >> 16);
62 out.buf[o++] = (uint8_t)(dl >> 8);
63 out.buf[o++] = (uint8_t)(dl);
64 memcpy(out.buf + o, desc, dl);
65 o += dl;
66 out.buf[o++] = 0; // language tag: empty string (4-byte length 0)
67 out.buf[o++] = 0;
68 out.buf[o++] = 0;
69 out.buf[o++] = 0;
70 emit(i, out.buf, o);
71}
72
73int pc_ssh_server_dispatch(uint8_t i, uint8_t msg_type, const uint8_t *payload, size_t len)
74{
75 if (i >= MAX_SSH_CONNS)
76 {
77 return -1;
78 }
79 SshSession *s = &ssh_sess[i];
80
81 // The reply buffer is borrowed for this dispatch, not carried on the worker stack: it is the
82 // single largest frame on the SSH path and the handshake below it (curve25519 / ed25519) is
83 // already the deepest call chain in the library. pc_scratch_span binds the capacity to the
84 // allocation, so every `reply.cap` below is the number that was actually reserved.
86 const pc_span &reply = reply_b.span();
87 if (!pc_span_ok(reply))
88 {
89 return -1; // arena exhausted: fail closed, the caller drops the connection
90 }
91 size_t n = 0;
92
93 switch (msg_type)
94 {
95 case SSH_MSG_IGNORE:
96 return 0;
97
99 return -1; // peer is closing
100
101 case SSH_MSG_KEXINIT:
102 // Initial KEX or an in-session re-key request. Negotiate, reply with our
103 // own KEXINIT, generate a fresh ephemeral, and await KEXDH_INIT.
104 if (ssh_kexinit_parse(i, payload, len) != 0)
105 {
106 return -1;
107 }
108 if (ssh_kexinit_build(i, reply.buf, &n, reply.cap) != 0) // GCOVR_EXCL_LINE cannot fail: i is checked above and
109 {
110 return -1; // GCOVR_EXCL_LINE buf is SSH_PKT_BUF_SIZE
111 }
112 emit(i, reply.buf, n);
113 if (ssh_kex_generate(i) != 0) // GCOVR_EXCL_LINE cannot fail: i is checked above and every method's
114 {
115 return -1; // GCOVR_EXCL_LINE ephemeral generation is infallible for i < MAX_SSH_CONNS
116 }
118 return 0;
119
122 {
123 return -1;
124 }
125 if (ssh_kexdh_handle(i, payload, len, reply.buf, &n, reply.cap) != 0)
126 {
127 return -1;
128 }
129 emit(i, reply.buf, n); // KEXDH_REPLY
130 {
131 uint8_t newkeys = SSH_MSG_NEWKEYS;
132 emit(i, &newkeys, 1); // server NEWKEYS (this one still goes out unencrypted)
133 ssh_newkeys_sent(i); // ...but our outbound is encrypted from the next packet on
134 }
135 return 0; // ssh_kexdh_handle advanced phase to NEWKEYS
136
137 case SSH_MSG_NEWKEYS:
138 ssh_newkeys_complete(i); // activate encryption; → SERVICE or OPEN
139 // RFC 8308: with encryption now active, advertise the signature algorithms
140 // we accept for pubkey userauth (server-sig-algs) so a modern client will
141 // sign an RSA key - it otherwise reports "no mutual signature algorithm".
142 // First encrypted message, before the client's SERVICE_REQUEST.
143 if (s->ext_info_c && ssh_extinfo_build(reply.buf, &n, reply.cap) == 0) // GCOVR_EXCL_LINE the build half cannot
144 {
145 emit(i, reply.buf, n); // fail: EXT_INFO is ~90 bytes and buf is SSH_PKT_BUF_SIZE
146 }
147 return 0;
148
149 case SSH_MSG_EXT_INFO:
150 return 0; // RFC 8308: a client may send its own EXT_INFO; we ignore it
151
153 // RFC 4253 §10: a service request is only valid once the key exchange has completed (NEWKEYS
154 // advances the phase to SSH_PHASE_SERVICE and turns on encryption). Rejecting it in any earlier
155 // phase stops a client from jumping from DH_INIT straight to userauth in cleartext, skipping the
156 // whole key exchange + host-key verification. Found by the pentest's ssh_msgtype_abuse.
158 {
159 return -1;
160 }
161 if (pc_ssh_auth_handle_service_request(payload, len, reply.buf, &n, reply.cap) != 0)
162 {
163 return -1;
164 }
165 emit(i, reply.buf, n);
167 return 0;
168
171 {
172 return -1;
173 }
174 if (pc_ssh_auth_handle_request(i, payload, len, reply.buf, &n, reply.cap) != 0)
175 {
176 return -1;
177 }
178 emit(i, reply.buf, n); // SUCCESS (→ phase OPEN), PK_OK probe, or FAILURE
179#if PC_ENABLE_SSH_ZLIB
180 // zlib@openssh.com: the compression stream starts on the FIRST packet AFTER USERAUTH_SUCCESS
181 // (which itself just went out uncompressed). Idempotent - a later re-auth cannot restart it.
182 if (n > 0 && reply.buf[0] == SSH_MSG_USERAUTH_SUCCESS) // GCOVR_EXCL_LINE n > 0 always: every handler that
183 {
184 ssh_comp_on_auth_success(i); // returns 0 has written a reply
185 }
186#endif
187 // Brute-force defense (RFC 4252 §4): bound failed attempts per connection. Only an actual
188 // USERAUTH_FAILURE counts - a SUCCESS or the publickey PK_OK probe does not. Too many ->
189 // DISCONNECT then close.
190 if (n > 0 && reply.buf[0] == SSH_MSG_USERAUTH_FAILURE) // GCOVR_EXCL_LINE n > 0 always (see above)
191 {
193 {
194 emit_auth_failure_disconnect(i, reply);
195 return -1; // close the connection
196 }
197 }
198 return 0;
199
200#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
202 // RFC 4256 §3.4: the client's answer to our keyboard-interactive prompt. Only valid mid-auth,
203 // and only when an exchange was armed (the handler enforces the latter). Same SUCCESS/FAILURE
204 // accounting as a USERAUTH_REQUEST: SUCCESS starts s2c compression and advances the phase; a
205 // FAILURE counts toward the brute-force limit.
207 {
208 return -1;
209 }
210 if (pc_ssh_auth_handle_info_response(i, payload, len, reply.buf, &n, reply.cap) != 0)
211 {
212 return -1;
213 }
214 emit(i, reply.buf, n);
215#if PC_ENABLE_SSH_ZLIB
216 if (n > 0 && reply.buf[0] == SSH_MSG_USERAUTH_SUCCESS) // GCOVR_EXCL_LINE n > 0 always (see above)
217 {
218 ssh_comp_on_auth_success(i);
219 }
220#endif
221 if (n > 0 && reply.buf[0] == SSH_MSG_USERAUTH_FAILURE) // GCOVR_EXCL_LINE n > 0 always (see above)
222 {
224 {
225 emit_auth_failure_disconnect(i, reply);
226 return -1;
227 }
228 }
229 return 0;
230#endif
231
233 // RFC 4254 §4: connection-wide request (e.g. tcpip-forward for ssh -R). Only
234 // meaningful post-auth; reply REQUEST_SUCCESS/FAILURE when want_reply is set.
235 if (!s->authed)
236 {
237 return -1;
238 }
239 if (ssh_global_request_handle(i, payload, len, reply.buf, &n, reply.cap) != 0)
240 {
241 return -1;
242 }
243 if (n > 0)
244 {
245 emit(i, reply.buf, n);
246 }
247 return 0;
248
250 if (!s->authed)
251 {
252 return -1;
253 }
254 if (pc_ssh_channel_handle_open(i, payload, len, reply.buf, &n, reply.cap) != 0)
255 {
256 return -1;
257 }
258 emit(i, reply.buf, n);
259 return 0;
260
262 // The client's reply to a server-initiated forwarded-tcpip open (ssh -R): record
263 // the peer window and start the bridge. A stray confirm is ignored (not fatal).
264 if (!s->authed)
265 {
266 return -1;
267 }
268 pc_ssh_channel_handle_open_confirm(i, payload, len);
269 return 0;
270
272 // The client refused a server-initiated forwarded-tcpip open: tear the bridge down.
273 if (!s->authed)
274 {
275 return -1;
276 }
277 pc_ssh_channel_handle_open_failure(i, payload, len);
278 return 0;
279
281 if (!s->authed)
282 {
283 return -1;
284 }
285 if (pc_ssh_channel_handle_request(i, payload, len, reply.buf, &n, reply.cap) != 0)
286 {
287 return -1;
288 }
289 emit(i, reply.buf, n); // SUCCESS/FAILURE only when want_reply was set
290 return 0;
291
293 if (!s->authed)
294 {
295 return -1;
296 }
297 if (pc_ssh_channel_handle_data(i, payload, len, reply.buf, &n, reply.cap) != 0)
298 {
299 return -1;
300 }
301 emit(i, reply.buf, n); // WINDOW_ADJUST when the receive window is replenished
302 return 0;
303
306 return 0;
307
309 return 0;
310
312 // handle_close frames CHANNEL_EOF + CHANNEL_CLOSE back to back, but each SSH
313 // message must travel in its own binary packet (RFC 4253 6): a strict peer
314 // runs packet_check_eom() after every message and rejects two in one. Emit
315 // the two halves as separate packets.
316 // GCOVR_EXCL_START n == 10 cannot be false: handle_close only returns 0 after writing exactly the
317 // ten bytes of CHANNEL_EOF + CHANNEL_CLOSE, so that half of the guard is unreachable.
318 if (pc_ssh_channel_handle_close(i, payload, len, reply.buf, &n, reply.cap) == 0 && n == 10)
319 {
320 emit(i, reply.buf, 5); // CHANNEL_EOF
321 emit(i, reply.buf + 5, 5); // CHANNEL_CLOSE
322 }
323 // GCOVR_EXCL_STOP
324 return 0;
325
326 default: {
327 // RFC 4253 §11.4: reply to an unrecognized message with
328 // SSH_MSG_UNIMPLEMENTED carrying the rejected packet's sequence number.
329 // ssh_pkt_recv() has already incremented seq_no_recv past this packet,
330 // so the rejected packet's number is seq_no_recv - 1.
331 uint32_t rej = ssh_pkt[i].seq_no_recv - 1u;
332 reply.buf[0] = SSH_MSG_UNIMPLEMENTED;
333 reply.buf[1] = (uint8_t)(rej >> 24);
334 reply.buf[2] = (uint8_t)(rej >> 16);
335 reply.buf[3] = (uint8_t)(rej >> 8);
336 reply.buf[4] = (uint8_t)(rej);
337 emit(i, reply.buf, 5);
338 return 0;
339 }
340 }
341}
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
A scratch borrow whose acquire and release are one call each.
Definition scratch.h:168
const pc_span & span() const
The borrowed region; empty if the arena could not satisfy it.
Definition scratch.h:176
#define SSH_MAX_AUTH_ATTEMPTS
Maximum failed SSH authentication attempts per connection.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
int pc_ssh_auth_handle_service_request(const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_SERVICE_REQUEST; emit SERVICE_ACCEPT for ssh-userauth.
Definition ssh_auth.cpp:258
int pc_ssh_auth_handle_request(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle a USERAUTH_REQUEST end-to-end for slot i.
Definition ssh_auth.cpp:583
SSH user-authentication layer (RFC 4252).
int pc_ssh_channel_handle_window_adjust(uint8_t i, const uint8_t *payload, size_t len)
Handle SSH_MSG_CHANNEL_WINDOW_ADJUST (grows the peer window).
int pc_ssh_channel_handle_close(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle an inbound SSH_MSG_CHANNEL_CLOSE: route to the recipient channel, reply with EOF + CLOSE,...
int pc_ssh_channel_handle_open(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_CHANNEL_OPEN and emit CHANNEL_OPEN_CONFIRMATION.
int pc_ssh_channel_handle_data(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_CHANNEL_DATA: bounds-check, update the window, and invoke the data callback....
int ssh_global_request_handle(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_GLOBAL_REQUEST (RFC 4254 §4).
int pc_ssh_channel_handle_open_confirm(uint8_t i, const uint8_t *payload, size_t len)
Handle SSH_MSG_CHANNEL_OPEN_CONFIRMATION for a channel we opened (ssh -R).
int pc_ssh_channel_handle_open_failure(uint8_t i, const uint8_t *payload, size_t len)
Handle SSH_MSG_CHANNEL_OPEN_FAILURE for a channel we opened (ssh -R).
int pc_ssh_channel_handle_request(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_CHANNEL_REQUEST.
SSH connection protocol - multiplexed "session" channels (RFC 4254).
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
DH-group14-SHA256 key exchange (RFC 4253 §8 + RFC 8268).
SshPacketState ssh_pkt[MAX_SSH_CONNS]
Static packet state pool (BSS). One entry per SSH slot.
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_MSG_CHANNEL_EOF
Definition ssh_packet.h:122
#define SSH_MSG_CHANNEL_DATA
Definition ssh_packet.h:121
#define SSH_MSG_NEWKEYS
Definition ssh_packet.h:103
#define SSH_MSG_USERAUTH_INFO_RESPONSE
Definition ssh_packet.h:113
#define SSH_MSG_DISCONNECT
Definition ssh_packet.h:96
#define SSH_MSG_CHANNEL_OPEN_CONFIRM
Definition ssh_packet.h:118
#define SSH_MSG_UNIMPLEMENTED
Definition ssh_packet.h:98
#define SSH_MSG_KEXINIT
Definition ssh_packet.h:102
#define SSH_MSG_USERAUTH_REQUEST
Definition ssh_packet.h:106
#define SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE
Definition ssh_packet.h:135
#define SSH_MSG_IGNORE
Definition ssh_packet.h:97
#define SSH_MSG_CHANNEL_OPEN
Definition ssh_packet.h:117
#define SSH_MSG_CHANNEL_REQUEST
Definition ssh_packet.h:124
#define SSH_MSG_CHANNEL_OPEN_FAILURE
Definition ssh_packet.h:119
#define SSH_MSG_SERVICE_REQUEST
Definition ssh_packet.h:99
#define SSH_MSG_KEXDH_INIT
Definition ssh_packet.h:104
#define SSH_MSG_USERAUTH_SUCCESS
Definition ssh_packet.h:108
#define SSH_MSG_CHANNEL_CLOSE
Definition ssh_packet.h:123
#define SSH_MSG_USERAUTH_FAILURE
Definition ssh_packet.h:107
#define SSH_MSG_GLOBAL_REQUEST
Definition ssh_packet.h:114
#define SSH_MSG_CHANNEL_WINDOW_ADJUST
Definition ssh_packet.h:120
#define SSH_MSG_EXT_INFO
Definition ssh_packet.h:101
void pc_ssh_server_set_emit_cb(SshEmitCb cb)
Install the outbound-message callback.
int pc_ssh_server_dispatch(uint8_t i, uint8_t msg_type, const uint8_t *payload, size_t len)
Dispatch one decrypted inbound SSH message for slot i.
SSH message dispatcher - ties the transport, auth, and channel layers into one server state machine.
void(* SshEmitCb)(uint8_t slot, const uint8_t *payload, size_t len)
Emit one outbound SSH message payload for slot slot.
Definition ssh_server.h:34
int ssh_kex_generate(uint8_t i)
Generate the server ephemeral for the negotiated KEX method (call after parse).
int ssh_extinfo_build(uint8_t *out, size_t *len, size_t cap)
Build SSH_MSG_EXT_INFO advertising server-sig-algs (RFC 8308).
int ssh_kexinit_build(uint8_t i, uint8_t *payload, size_t *len, size_t cap)
Build the server KEXINIT payload for slot i (RFC 4253 §7.1).
void ssh_newkeys_complete(uint8_t i)
Complete the NEWKEYS exchange: activate the inbound direction and advance phase.
void ssh_newkeys_sent(uint8_t i)
Activate the outbound direction after emitting our SSH_MSG_NEWKEYS.
SshSession ssh_sess[MAX_SSH_CONNS]
Static pool of SSH session state (BSS), one per SSH slot.
int ssh_kexinit_parse(uint8_t i, const uint8_t *payload, size_t len)
Parse and negotiate the client KEXINIT payload (RFC 4253 §7.1).
int ssh_kexdh_handle(uint8_t i, const uint8_t *payload, size_t len, uint8_t *reply_out, size_t *reply_len, size_t cap)
Handle KEXDH/ECDH_INIT (msg 30) end-to-end and produce the reply payload.
SSH transport-layer protocol state machine (RFC 4253).
@ SSH_PHASE_AUTH
User authentication in progress (RFC 4252).
@ SSH_PHASE_SERVICE
Awaiting SERVICE_REQUEST ("ssh-userauth").
@ SSH_PHASE_DH_INIT
Awaiting SSH_MSG_KEXDH_INIT.
uint32_t seq_no_recv
Incoming sequence number (incremented per packet).
Definition ssh_packet.h:151
SshEmitCb emit_cb
bool ext_info_c
Client advertised ext-info-c (RFC 8308): send EXT_INFO.
SshPhase phase
Current handshake phase.
bool authed
True after successful user authentication.
uint8_t auth_failures
Failed USERAUTH_REQUESTs (brute-force limit, RFC 4252 §4).
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66
size_t cap
bytes writable at buf (0 whenever buf is nullptr)
Definition span.h:67