DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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#if DETWS_ENABLE_SSH_ZLIB
17#endif
18#include <string.h>
19
20// All SSH server-layer state, owned by one instance (internal linkage): the packet-emit
21// callback. One named owner, unreachable from any other translation unit.
23{
24 SshEmitCb emit_cb = nullptr;
25};
26static SshServerCtx s_srv;
27
29{
30 s_srv.emit_cb = cb;
31}
32
33static inline void emit(uint8_t i, const uint8_t *p, size_t n)
34{
35 if (s_srv.emit_cb && n > 0)
36 s_srv.emit_cb(i, p, n);
37}
38
39// Build and emit SSH_MSG_DISCONNECT("too many authentication failures") - the reason code, the
40// description string, and an empty language tag (RFC 4253 §11.1) - then the caller closes.
41static void emit_auth_failure_disconnect(uint8_t i, uint8_t *buf)
42{
43 static const char desc[] = "too many authentication failures";
45 const uint32_t dl = (uint32_t)(sizeof(desc) - 1);
46 size_t o = 0;
47 buf[o++] = SSH_MSG_DISCONNECT;
48 buf[o++] = (uint8_t)(reason >> 24); // reason code (uint32, big-endian)
49 buf[o++] = (uint8_t)(reason >> 16);
50 buf[o++] = (uint8_t)(reason >> 8);
51 buf[o++] = (uint8_t)(reason);
52 buf[o++] = (uint8_t)(dl >> 24); // description (uint32 length + bytes)
53 buf[o++] = (uint8_t)(dl >> 16);
54 buf[o++] = (uint8_t)(dl >> 8);
55 buf[o++] = (uint8_t)(dl);
56 memcpy(buf + o, desc, dl);
57 o += dl;
58 buf[o++] = 0; // language tag: empty string (4-byte length 0)
59 buf[o++] = 0;
60 buf[o++] = 0;
61 buf[o++] = 0;
62 emit(i, buf, o);
63}
64
65int ssh_server_dispatch(uint8_t i, uint8_t msg_type, const uint8_t *payload, size_t len)
66{
67 if (i >= MAX_SSH_CONNS)
68 return -1;
69 SshSession *s = &ssh_sess[i];
70
71 uint8_t buf[SSH_PKT_BUF_SIZE];
72 size_t n = 0;
73
74 switch (msg_type)
75 {
76 case SSH_MSG_IGNORE:
77 return 0;
78
80 return -1; // peer is closing
81
82 case SSH_MSG_KEXINIT:
83 // Initial KEX or an in-session re-key request. Negotiate, reply with our
84 // own KEXINIT, generate a fresh ephemeral, and await KEXDH_INIT.
85 if (ssh_kexinit_parse(i, payload, len) != 0)
86 return -1;
87 if (ssh_kexinit_build(i, buf, &n, sizeof(buf)) != 0)
88 return -1;
89 emit(i, buf, n);
90 if (ssh_kex_generate(i) != 0) // ephemeral for the just-negotiated KEX method
91 return -1;
93 return 0;
94
97 return -1;
98 if (ssh_kexdh_handle(i, payload, len, buf, &n, sizeof(buf)) != 0)
99 return -1;
100 emit(i, buf, n); // KEXDH_REPLY
101 {
102 uint8_t newkeys = SSH_MSG_NEWKEYS;
103 emit(i, &newkeys, 1); // server NEWKEYS (this one still goes out unencrypted)
104 ssh_newkeys_sent(i); // ...but our outbound is encrypted from the next packet on
105 }
106 return 0; // ssh_kexdh_handle advanced phase to NEWKEYS
107
108 case SSH_MSG_NEWKEYS:
109 ssh_newkeys_complete(i); // activate encryption; → SERVICE or OPEN
110 // RFC 8308: with encryption now active, advertise the signature algorithms
111 // we accept for pubkey userauth (server-sig-algs) so a modern client will
112 // sign an RSA key - it otherwise reports "no mutual signature algorithm".
113 // First encrypted message, before the client's SERVICE_REQUEST.
114 if (s->ext_info_c && ssh_extinfo_build(buf, &n, sizeof(buf)) == 0)
115 emit(i, buf, n);
116 return 0;
117
118 case SSH_MSG_EXT_INFO:
119 return 0; // RFC 8308: a client may send its own EXT_INFO; we ignore it
120
122 // RFC 4253 §10: a service request is only valid once the key exchange has completed (NEWKEYS
123 // advances the phase to SSH_PHASE_SERVICE and turns on encryption). Rejecting it in any earlier
124 // phase stops a client from jumping from DH_INIT straight to userauth in cleartext, skipping the
125 // whole key exchange + host-key verification. Found by the pentest's ssh_msgtype_abuse.
127 return -1;
128 if (ssh_auth_handle_service_request(payload, len, buf, &n, sizeof(buf)) != 0)
129 return -1;
130 emit(i, buf, n);
132 return 0;
133
136 return -1;
137 if (ssh_auth_handle_request(i, payload, len, buf, &n, sizeof(buf)) != 0)
138 return -1;
139 emit(i, buf, n); // SUCCESS (→ phase OPEN), PK_OK probe, or FAILURE
140#if DETWS_ENABLE_SSH_ZLIB
141 // zlib@openssh.com: the compression stream starts on the FIRST packet AFTER USERAUTH_SUCCESS
142 // (which itself just went out uncompressed). Idempotent - a later re-auth cannot restart it.
143 if (n > 0 && buf[0] == SSH_MSG_USERAUTH_SUCCESS)
144 ssh_comp_on_auth_success(i);
145#endif
146 // Brute-force defense (RFC 4252 §4): bound failed attempts per connection. Only an actual
147 // USERAUTH_FAILURE counts - a SUCCESS or the publickey PK_OK probe does not. Too many ->
148 // DISCONNECT then close.
149 if (n > 0 && buf[0] == SSH_MSG_USERAUTH_FAILURE)
150 {
152 {
153 emit_auth_failure_disconnect(i, buf);
154 return -1; // close the connection
155 }
156 }
157 return 0;
158
160 // RFC 4254 §4: connection-wide request (e.g. tcpip-forward for ssh -R). Only
161 // meaningful post-auth; reply REQUEST_SUCCESS/FAILURE when want_reply is set.
162 if (!s->authed)
163 return -1;
164 if (ssh_global_request_handle(i, payload, len, buf, &n, sizeof(buf)) != 0)
165 return -1;
166 if (n > 0)
167 emit(i, buf, n);
168 return 0;
169
171 if (!s->authed)
172 return -1;
173 if (ssh_channel_handle_open(i, payload, len, buf, &n, sizeof(buf)) != 0)
174 return -1;
175 emit(i, buf, n);
176 return 0;
177
179 // The client's reply to a server-initiated forwarded-tcpip open (ssh -R): record
180 // the peer window and start the bridge. A stray confirm is ignored (not fatal).
181 if (!s->authed)
182 return -1;
183 ssh_channel_handle_open_confirm(i, payload, len);
184 return 0;
185
187 // The client refused a server-initiated forwarded-tcpip open: tear the bridge down.
188 if (!s->authed)
189 return -1;
190 ssh_channel_handle_open_failure(i, payload, len);
191 return 0;
192
194 if (!s->authed)
195 return -1;
196 if (ssh_channel_handle_request(i, payload, len, buf, &n, sizeof(buf)) != 0)
197 return -1;
198 emit(i, buf, n); // SUCCESS/FAILURE only when want_reply was set
199 return 0;
200
202 if (!s->authed)
203 return -1;
204 if (ssh_channel_handle_data(i, payload, len, buf, &n, sizeof(buf)) != 0)
205 return -1;
206 emit(i, buf, n); // WINDOW_ADJUST when the receive window is replenished
207 return 0;
208
210 ssh_channel_handle_window_adjust(i, payload, len);
211 return 0;
212
214 return 0;
215
217 // handle_close frames CHANNEL_EOF + CHANNEL_CLOSE back to back, but each SSH
218 // message must travel in its own binary packet (RFC 4253 6): a strict peer
219 // runs packet_check_eom() after every message and rejects two in one. Emit
220 // the two halves as separate packets.
221 if (ssh_channel_handle_close(i, payload, len, buf, &n, sizeof(buf)) == 0 && n == 10)
222 {
223 emit(i, buf, 5); // CHANNEL_EOF
224 emit(i, buf + 5, 5); // CHANNEL_CLOSE
225 }
226 return 0;
227
228 default: {
229 // RFC 4253 §11.4: reply to an unrecognized message with
230 // SSH_MSG_UNIMPLEMENTED carrying the rejected packet's sequence number.
231 // ssh_pkt_recv() has already incremented seq_no_recv past this packet,
232 // so the rejected packet's number is seq_no_recv - 1.
233 uint32_t rej = ssh_pkt[i].seq_no_recv - 1u;
234 buf[0] = SSH_MSG_UNIMPLEMENTED;
235 buf[1] = (uint8_t)(rej >> 24);
236 buf[2] = (uint8_t)(rej >> 16);
237 buf[3] = (uint8_t)(rej >> 8);
238 buf[4] = (uint8_t)(rej);
239 emit(i, buf, 5);
240 return 0;
241 }
242 }
243}
#define SSH_MAX_AUTH_ATTEMPTS
Maximum failed SSH authentication attempts per connection.
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
int 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:408
int 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:194
SSH user-authentication layer (RFC 4252).
int 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 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 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 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 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.
int 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 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).
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:118
#define SSH_MSG_CHANNEL_DATA
Definition ssh_packet.h:117
#define SSH_MSG_NEWKEYS
Definition ssh_packet.h:103
#define SSH_MSG_DISCONNECT
Definition ssh_packet.h:96
#define SSH_MSG_CHANNEL_OPEN_CONFIRM
Definition ssh_packet.h:114
#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:131
#define SSH_MSG_IGNORE
Definition ssh_packet.h:97
#define SSH_MSG_CHANNEL_OPEN
Definition ssh_packet.h:113
#define SSH_MSG_CHANNEL_REQUEST
Definition ssh_packet.h:120
#define SSH_MSG_CHANNEL_OPEN_FAILURE
Definition ssh_packet.h:115
#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:119
#define SSH_MSG_USERAUTH_FAILURE
Definition ssh_packet.h:107
#define SSH_MSG_GLOBAL_REQUEST
Definition ssh_packet.h:110
#define SSH_MSG_CHANNEL_WINDOW_ADJUST
Definition ssh_packet.h:116
#define SSH_MSG_EXT_INFO
Definition ssh_packet.h:101
int 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.
void ssh_server_set_emit_cb(SshEmitCb cb)
Install the outbound-message callback.
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:147
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).