DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_transport.h
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_transport.h
6 * @brief SSH transport-layer protocol state machine (RFC 4253).
7 *
8 * Sits on top of the binary packet layer (ssh_packet.*) and the crypto
9 * primitives (ssh_dh, ssh_rsa, ssh_aes256ctr, ssh_hmac_sha256). Drives the
10 * handshake: identification-string (banner) exchange → algorithm negotiation
11 * (KEXINIT) → Diffie-Hellman key exchange (KEXDH) → NEWKEYS → key install,
12 * then hands off to the user-auth layer (ssh_auth.*).
13 *
14 * ── Supported algorithms (crypto-agnostic KEX; steered to a runtime preference) ─
15 * kex : diffie-hellman-group14-sha256 (RFC 8268)
16 * curve25519-sha256 (RFC 8731)
17 * ecdh-sha2-nistp256 (RFC 5656 §4)
18 * host key / sig : rsa-sha2-512, rsa-sha2-256 (RFC 8332)
19 * ecdsa-sha2-nistp256 (RFC 5656)
20 * ssh-ed25519 (RFC 8709)
21 * cipher (both) : aes256-ctr (RFC 4344)
22 * MAC (both) : hmac-sha2-256 (RFC 6668)
23 * compression : none
24 *
25 * KEX method and host-key type are negotiated: the server advertises both suites in
26 * ssh_kex_set_prefer_rsa() order (default: RSA/DH, hardware-accelerated on ESP32) and
27 * picks the first mutually supported one it holds a key for. Cipher / MAC / compression
28 * are fixed; the connection is accepted only if the client offers each of those.
29 *
30 * @author Douglas Quigg (dstroy0)
31 * @date 2026
32 */
33
34#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_TRANSPORT_H
35#define DETERMINISTICESPASYNCWEBSERVER_SSH_TRANSPORT_H
36
37#include "ServerConfig.h"
40#include <stddef.h>
41#include <stdint.h>
42
43// ---------------------------------------------------------------------------
44// Sizing
45// ---------------------------------------------------------------------------
46
47/** @brief Max stored length of an SSH identification string (RFC 4253 §4.2: 255). */
48#define SSH_VERSION_MAX 256
49
50/** @brief Max stored size of our own KEXINIT (I_S). Sized for the full advertised suite: the
51 * kex list (mlkem + dh + ecdh-nistp256 + curve25519 x2 + ext-info-s), all three host-key types,
52 * the cipher (chacha + 2x aes) and MAC (2x etm + 2x plain) lists, and zlib s2c compression
53 * (worst case ~580 bytes; 704 leaves headroom for future algorithm additions). */
54static constexpr size_t SSH_KEXINIT_S_MAX = 704;
55
56/** @brief Server identification string (no CR LF; appended on the wire). */
57#define SSH_SERVER_VERSION "SSH-2.0-DetWS_1.0"
58
59// ---------------------------------------------------------------------------
60// Handshake phase
61// ---------------------------------------------------------------------------
62
63/** @brief SSH connection lifecycle phase. */
64enum class SshPhase : uint8_t
65{
66 SSH_PHASE_BANNER, ///< Awaiting the client identification string.
67 SSH_PHASE_KEXINIT, ///< Awaiting the client KEXINIT.
68 SSH_PHASE_DH_INIT, ///< Awaiting SSH_MSG_KEXDH_INIT.
69 SSH_PHASE_NEWKEYS, ///< Awaiting SSH_MSG_NEWKEYS.
70 SSH_PHASE_SERVICE, ///< Awaiting SERVICE_REQUEST ("ssh-userauth").
71 SSH_PHASE_AUTH, ///< User authentication in progress (RFC 4252).
72 SSH_PHASE_OPEN ///< Authenticated; connection/channel protocol active.
73};
74
75// ---------------------------------------------------------------------------
76// Per-connection transport state
77// ---------------------------------------------------------------------------
78
79/**
80 * @brief SSH transport/session state for one connection (BSS pool).
81 *
82 * Holds the handshake phase plus the few values that must persist across
83 * messages to compute the exchange hash H: the client and server
84 * identification strings (V_C, V_S) and the two KEXINIT payloads (I_C, I_S).
85 * The exchange hash from the first KEX is retained as the session id, which
86 * is required for key derivation and for every later re-key.
87 */
88/** @brief Negotiated key-exchange method (crypto-agnostic KEX dispatch). */
89enum class SshKexAlg : uint8_t
90{
91 SSH_KEX_DH_GROUP14 = 0, ///< diffie-hellman-group14-sha256 (HW-accelerated MPI on ESP32)
92 SSH_KEX_CURVE25519 = 1, ///< curve25519-sha256 (RFC 8731, X25519)
93 SSH_KEX_MLKEM768_X25519 = 2, ///< mlkem768x25519-sha256 (PQ/T hybrid, draft-ietf-sshm-mlkem-hybrid-kex)
94 SSH_KEX_ECDH_NISTP256 = 3 ///< ecdh-sha2-nistp256 (NIST P-256 ECDH, RFC 5656 §4)
95};
96
97/** @brief Negotiated host-key / signature algorithm. */
98enum class SshHostkeyAlg : uint8_t
99{
100 SSH_HOSTKEY_RSA_SHA256 = 0, ///< rsa-sha2-256 (HW-accelerated on ESP32)
101 SSH_HOSTKEY_ED25519 = 1, ///< ssh-ed25519 (RFC 8032)
102 SSH_HOSTKEY_RSA_SHA512 = 2, ///< rsa-sha2-512 (same "ssh-rsa" key, SHA-512 signature; RFC 8332)
103 SSH_HOSTKEY_ECDSA_NISTP256 = 3 ///< ecdsa-sha2-nistp256 (NIST P-256, RFC 5656)
104};
105
107{
108 SshPhase phase; ///< Current handshake phase.
109
110 SshKexAlg kex_alg; ///< negotiated in KEXINIT.
111 SshHostkeyAlg hostkey_alg; ///< negotiated in KEXINIT.
112 uint8_t cipher_alg; ///< SSH_CIPHER_* negotiated in KEXINIT (0 = aes256-ctr).
113 uint8_t mac_alg; ///< SSH_MAC_* negotiated in KEXINIT (aes cipher only; 0 = hmac-sha2-256).
114 uint8_t ecdh_sk[32]; ///< Server X25519 ephemeral private (curve25519 KEX only; wiped after).
115 uint8_t ecdh_pk[32]; ///< Server X25519 ephemeral public (curve25519 KEX only).
116
117 char v_c[SSH_VERSION_MAX]; ///< Client identification string (no CR LF).
118 uint16_t v_c_len; ///< Length of v_c.
119
120 uint8_t banner_buf[SSH_VERSION_MAX]; ///< Accumulator for the inbound banner.
121 uint16_t banner_len; ///< Bytes buffered in banner_buf.
122
123 uint8_t i_c[SSH_KEXINIT_MAX]; ///< Client KEXINIT payload (for H).
124 uint16_t i_c_len; ///< Length of i_c.
125 uint8_t i_s[SSH_KEXINIT_S_MAX]; ///< Server KEXINIT payload (for H).
126 uint16_t i_s_len; ///< Length of i_s.
127
128 uint8_t session_id[SSH_SHA256_DIGEST_LEN]; ///< H from the first KEX (RFC 4253 §7.2).
129 bool have_session_id; ///< True once the first KEX completes.
130
131 bool ext_info_c; ///< Client advertised ext-info-c (RFC 8308): send EXT_INFO.
132 bool authed; ///< True after successful user authentication.
133 uint8_t auth_failures; ///< Failed USERAUTH_REQUESTs (brute-force limit, RFC 4252 §4).
134 uint32_t last_kex_ms; ///< detws_millis() when the last KEX completed (server-initiated re-key timer).
135};
136
137/** @brief Static pool of SSH session state (BSS), one per SSH slot. */
139
140// ---------------------------------------------------------------------------
141// API
142// ---------------------------------------------------------------------------
143
144/**
145 * @brief Reset transport state for slot @p i to the start of a handshake.
146 */
147void ssh_transport_init(uint8_t i);
148
149/**
150 * @brief Write the server identification string ("SSH-2.0-…\r\n") to @p out.
151 *
152 * Sent verbatim (not inside a binary packet) at connection start, before any
153 * KEXINIT. The CR LF is included on the wire but excluded from V_S used in H.
154 *
155 * @return 0 on success, -1 if @p cap is too small.
156 */
157int ssh_transport_server_banner(uint8_t *out, size_t *out_len, size_t cap);
158
159/**
160 * @brief Feed raw bytes while awaiting the client identification string.
161 *
162 * Accumulates bytes until a CR LF (or bare LF) terminates a line beginning
163 * with "SSH-". Earlier non-SSH lines (allowed by RFC 4253 §4.2) are skipped.
164 * On completion the client version (without CR LF) is stored in v_c.
165 *
166 * @param[in] i SSH slot.
167 * @param[in] data Inbound bytes.
168 * @param[in] len Number of bytes in @p data.
169 * @param[out] consumed Bytes consumed from @p data (the banner may be followed
170 * immediately by binary packets).
171 * @return 1 when the banner is complete, 0 if more data is needed, -1 on error.
172 */
173int ssh_transport_recv_banner(uint8_t i, const uint8_t *data, size_t len, size_t *consumed);
174
175/**
176 * @brief Build the server KEXINIT payload for slot @p i (RFC 4253 §7.1).
177 *
178 * Stores a copy in ssh_sess[i].i_s for later exchange-hash computation.
179 *
180 * @return 0 on success, -1 on buffer overflow.
181 */
182int ssh_kexinit_build(uint8_t i, uint8_t *payload, size_t *len, size_t cap);
183
184/**
185 * @brief Parse and negotiate the client KEXINIT payload (RFC 4253 §7.1).
186 *
187 * Stores a copy in ssh_sess[i].i_c. Verifies the client offers every algorithm
188 * this server supports (one per category).
189 *
190 * @return 0 if negotiation succeeds, -1 if any required algorithm is absent or
191 * the payload is malformed.
192 */
193int ssh_kexinit_parse(uint8_t i, const uint8_t *payload, size_t len);
194
195/**
196 * @brief Steer KEX / host-key negotiation toward RSA + DH-group14 (default) or toward
197 * the modern curve25519 + ed25519 suite.
198 *
199 * On ESP32 the RSA/DH path runs on the hardware MPI accelerator, while curve25519 /
200 * ed25519 are software; a device that wants the accelerated handshake keeps the default
201 * (prefer RSA), while one that wants modern crypto out of the box calls this with false.
202 * The server still advertises both suites (for whatever keys it holds), so a client that
203 * only supports one still connects - this only sets the server's preference order.
204 *
205 * Runtime-selectable so one firmware can flip per deployment. Default: prefer RSA.
206 */
207void ssh_kex_set_prefer_rsa(bool prefer);
208
209/** @brief Current negotiation preference (true = prefer RSA/DH, the ESP32-accelerated path). */
210bool ssh_kex_prefer_rsa(void);
211
212/**
213 * @brief Install an ssh-ed25519 host key from its 32-byte seed (RFC 8032 private key).
214 *
215 * Enables the ssh-ed25519 host-key algorithm for negotiation and derives the public key.
216 * The RSA host key (loaded via ssh_rsa) and this may both be present; negotiation picks
217 * one per ssh_kex_set_prefer_rsa(). If neither is installed the handshake cannot complete.
218 */
219void ssh_hostkey_ed25519_set(const uint8_t seed[32]);
220
221/** @brief True if an ssh-ed25519 host key has been installed. */
223
224/**
225 * @brief Install an ecdsa-sha2-nistp256 host key from its 32-byte P-256 private scalar.
226 *
227 * Enables the ecdsa-sha2-nistp256 host-key algorithm (RFC 5656) for negotiation and derives
228 * the public point. May coexist with the RSA and ssh-ed25519 host keys; negotiation picks one
229 * per ssh_kex_set_prefer_rsa(). An invalid scalar (0 or >= the group order) is ignored.
230 */
231void ssh_hostkey_ecdsa_set(const uint8_t priv[32]);
232
233/** @brief True if an ecdsa-sha2-nistp256 host key has been installed. */
235
236/**
237 * @brief Generate the server ephemeral for the negotiated KEX method (call after parse).
238 *
239 * Branches on ssh_sess[i].kex_alg: for diffie-hellman-group14 it delegates to
240 * ssh_dh_generate(); for curve25519-sha256 it draws a random X25519 scalar and computes
241 * the matching public value into ssh_sess[i].ecdh_sk / ecdh_pk; for ecdh-sha2-nistp256 it
242 * draws a random P-256 scalar into ecdh_sk (the 65-byte public point is re-derived when the
243 * KEXDH_INIT arrives). Must run after ssh_kexinit_parse() has set kex_alg.
244 *
245 * @return 0 on success, -1 on error.
246 */
247int ssh_kex_generate(uint8_t i);
248
249/**
250 * @brief Build SSH_MSG_EXT_INFO advertising server-sig-algs (RFC 8308).
251 *
252 * Tells the client which public-key signature algorithms the server will accept
253 * for userauth (rsa-sha2-512, rsa-sha2-256, ssh-ed25519); without it a modern
254 * OpenSSH client refuses to sign an RSA key ("no mutual signature algorithm").
255 * Sent once, right after NEWKEYS, when the client advertised ext-info-c.
256 * @return 0 on success, -1 on buffer overflow.
257 */
258int ssh_extinfo_build(uint8_t *out, size_t *len, size_t cap);
259
260/**
261 * @brief Compute the SSH exchange hash H (RFC 4253 §8).
262 *
263 * H = SHA256( string(V_C) || string(V_S) || string(I_C) || string(I_S)
264 * || string(K_S) || mpint(e) || mpint(f) || mpint(K) )
265 *
266 * V_C/V_S/I_C/I_S are taken from ssh_sess[i]; the rest are supplied. The
267 * 256-byte big-endian integers are re-encoded as SSH mpints (minimal length,
268 * leading 0x00 when the high bit is set).
269 *
270 * @param[in] i SSH slot.
271 * @param[in] e_be Client DH public value e (256-byte big-endian).
272 * @param[in] f_be Server DH public value f (256-byte big-endian).
273 * @param[in] k_be Shared secret K (256-byte big-endian).
274 * @param[in] ks Server host-key blob K_S.
275 * @param[in] ks_len Length of @p ks.
276 * @param[out] out 32-byte exchange hash.
277 * @return 0 on success, -1 on bad slot.
278 */
279int ssh_kex_exchange_hash(uint8_t i, const uint8_t *e_be, const uint8_t *f_be, const uint8_t *k_be, const uint8_t *ks,
280 size_t ks_len, uint8_t out[SSH_SHA256_DIGEST_LEN]);
281
282/**
283 * @brief Parse SSH_MSG_KEXDH_INIT, extracting the client DH value e.
284 *
285 * Payload: byte(30) || mpint(e). The mpint is normalized into a fixed 256-byte
286 * big-endian buffer (leading sign/zero bytes stripped, right-aligned).
287 *
288 * @param[in] payload KEXDH_INIT payload.
289 * @param[in] len Payload length.
290 * @param[out] e_be 256-byte big-endian client public value.
291 * @return 0 on success, -1 if malformed or e exceeds 2048 bits.
292 */
293int ssh_kexdh_parse_init(const uint8_t *payload, size_t len, uint8_t e_be[256]);
294
295/**
296 * @brief Build SSH_MSG_KEXDH_REPLY (RFC 4253 §8, RFC 8332 §3).
297 *
298 * Payload: byte(31) || string(K_S) || mpint(f) || string(signature), where the
299 * signature blob is string("rsa-sha2-256") || string(@p sig).
300 *
301 * @param[in] ks Server host-key blob K_S.
302 * @param[in] ks_len Length of @p ks.
303 * @param[in] f_be Server DH public value f (256-byte big-endian).
304 * @param[in] sig Raw RSA signature over the exchange hash H.
305 * @param[in] sig_len Length of @p sig (256 for RSA-2048).
306 * @param[out] out Output payload buffer.
307 * @param[out] out_len Bytes written.
308 * @param[in] cap Capacity of @p out.
309 * @return 0 on success, -1 on overflow.
310 */
311int ssh_kexdh_build_reply(const uint8_t *ks, size_t ks_len, const uint8_t *f_be, const uint8_t *sig, size_t sig_len,
312 uint8_t *out, size_t *out_len, size_t cap);
313
314/**
315 * @brief Handle KEXDH/ECDH_INIT (msg 30) end-to-end and produce the reply payload.
316 *
317 * Branches on the negotiated KEX method (ssh_sess[i].kex_alg): computes the shared
318 * secret K = e^y mod p (DH-group14) or K = X25519(sk, Q_C) (curve25519), builds the
319 * method-correct exchange hash H (e/f as mpints for DH, Q_C/Q_S as strings for curve),
320 * signs H with the negotiated host key (rsa-sha2-512/256 or ssh-ed25519), assembles
321 * SSH_MSG_KEXDH_REPLY, and derives the six session keys (installed into ssh_keys[i];
322 * encryption is not activated until NEWKEYS - see ssh_newkeys_complete()). On the first
323 * KEX the exchange hash is saved as the session id. K is wiped from the stack before
324 * returning.
325 *
326 * Requires ssh_kex_generate(i) and a host key (ssh_rsa_load_pubkey() and/or
327 * ssh_hostkey_ed25519_set()) to have been called.
328 *
329 * @param[in] i SSH slot.
330 * @param[in] payload KEXDH_INIT payload.
331 * @param[in] len Payload length.
332 * @param[out] reply_out KEXDH_REPLY payload buffer.
333 * @param[out] reply_len Bytes written to @p reply_out.
334 * @param[in] cap Capacity of @p reply_out.
335 * @return 0 on success, -1 on validation/crypto/buffer error.
336 */
337int ssh_kexdh_handle(uint8_t i, const uint8_t *payload, size_t len, uint8_t *reply_out, size_t *reply_len, size_t cap);
338
339/**
340 * @brief Activate the outbound direction after emitting our SSH_MSG_NEWKEYS.
341 *
342 * Call this right after sending the server's NEWKEYS: it turns on the outbound cipher/MAC (enc_out) and
343 * starts the s2c compression stream. Per RFC 4253 sec 7.3 each direction activates independently, so the
344 * outbound turns on when we send, not when the peer's NEWKEYS arrives.
345 */
346void ssh_newkeys_sent(uint8_t i);
347
348/**
349 * @brief Complete the NEWKEYS exchange: activate the inbound direction and advance phase.
350 *
351 * Called once the client's SSH_MSG_NEWKEYS has been received (the server having already sent its own,
352 * via ssh_newkeys_sent()). Turns on the inbound cipher/MAC (enc_in), clears kex_active, and moves to
353 * SshPhase::SSH_PHASE_SERVICE (or back to SshPhase::SSH_PHASE_OPEN on a re-key).
354 */
355void ssh_newkeys_complete(uint8_t i);
356
357/**
358 * @brief True if slot @p i has reached the re-key threshold (RFC 4253 §9).
359 *
360 * Checks both packet sequence numbers against SSH_REKEY_PACKET_THRESHOLD.
361 */
362bool ssh_rekey_needed(uint8_t i);
363
364/**
365 * @brief Pure re-key decision (RFC 4253 §9: "after each gigabyte ... or after each hour").
366 *
367 * @param seq_send / @param seq_recv the outbound / inbound packet counters (a data-volume proxy).
368 * @param elapsed_ms milliseconds since the last KEX completed.
369 * @param pkt_threshold the packet-count trigger (SSH_REKEY_PACKET_THRESHOLD).
370 * @param time_threshold_ms the elapsed-time trigger (SSH_REKEY_TIME_MS); 0 disables the time trigger.
371 * @return true if either a packet counter or the elapsed time has crossed its threshold.
372 */
373bool ssh_rekey_due(uint32_t seq_send, uint32_t seq_recv, uint32_t elapsed_ms, uint32_t pkt_threshold,
374 uint32_t time_threshold_ms);
375
376/**
377 * @brief Begin a server-initiated re-key by emitting a fresh KEXINIT.
378 *
379 * Generates a new ephemeral DH key pair, builds and stores a new server
380 * KEXINIT (I_S), and returns the transport to SshPhase::SSH_PHASE_KEXINIT. The session
381 * id and authentication state are preserved, so once the re-key completes the
382 * connection resumes in its prior (authenticated) phase.
383 *
384 * @param[in] i Connection slot index.
385 * @param[out] out KEXINIT payload to send.
386 * @param[out] out_len Bytes written.
387 * @param[in] cap Capacity of @p out.
388 * @return 0 on success, -1 on error.
389 */
390int ssh_transport_begin_rekey(uint8_t i, uint8_t *out, size_t *out_len, size_t cap);
391
392#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_TRANSPORT_H
User-facing configuration for DeterministicESPAsyncWebServer.
#define SSH_KEXINIT_MAX
Max stored size of the CLIENT KEXINIT payload (I_C, for the exchange hash).
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
SSH session key material - types, pools, and security model.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29
int ssh_kex_generate(uint8_t i)
Generate the server ephemeral for the negotiated KEX method (call after parse).
bool ssh_kex_prefer_rsa(void)
Current negotiation preference (true = prefer RSA/DH, the ESP32-accelerated path).
bool ssh_hostkey_ed25519_available(void)
True if an ssh-ed25519 host key has been installed.
int ssh_transport_recv_banner(uint8_t i, const uint8_t *data, size_t len, size_t *consumed)
Feed raw bytes while awaiting the client identification string.
void ssh_hostkey_ed25519_set(const uint8_t seed[32])
Install an ssh-ed25519 host key from its 32-byte seed (RFC 8032 private key).
int ssh_extinfo_build(uint8_t *out, size_t *len, size_t cap)
Build SSH_MSG_EXT_INFO advertising server-sig-algs (RFC 8308).
#define SSH_VERSION_MAX
Max stored length of an SSH identification string (RFC 4253 §4.2: 255).
SshHostkeyAlg
Negotiated host-key / signature algorithm.
@ SSH_HOSTKEY_RSA_SHA512
rsa-sha2-512 (same "ssh-rsa" key, SHA-512 signature; RFC 8332)
@ SSH_HOSTKEY_ECDSA_NISTP256
ecdsa-sha2-nistp256 (NIST P-256, RFC 5656)
@ SSH_HOSTKEY_RSA_SHA256
rsa-sha2-256 (HW-accelerated on ESP32)
@ SSH_HOSTKEY_ED25519
ssh-ed25519 (RFC 8032)
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_hostkey_ecdsa_set(const uint8_t priv[32])
Install an ecdsa-sha2-nistp256 host key from its 32-byte P-256 private scalar.
void ssh_newkeys_complete(uint8_t i)
Complete the NEWKEYS exchange: activate the inbound direction and advance phase.
bool ssh_rekey_needed(uint8_t i)
True if slot i has reached the re-key threshold (RFC 4253 §9).
void ssh_newkeys_sent(uint8_t i)
Activate the outbound direction after emitting our SSH_MSG_NEWKEYS.
int ssh_transport_begin_rekey(uint8_t i, uint8_t *out, size_t *out_len, size_t cap)
Begin a server-initiated re-key by emitting a fresh KEXINIT.
bool ssh_hostkey_ecdsa_available(void)
True if an ecdsa-sha2-nistp256 host key has been installed.
SshKexAlg
SSH transport/session state for one connection (BSS pool).
@ SSH_KEX_MLKEM768_X25519
mlkem768x25519-sha256 (PQ/T hybrid, draft-ietf-sshm-mlkem-hybrid-kex)
@ SSH_KEX_ECDH_NISTP256
ecdh-sha2-nistp256 (NIST P-256 ECDH, RFC 5656 §4)
@ SSH_KEX_CURVE25519
curve25519-sha256 (RFC 8731, X25519)
@ SSH_KEX_DH_GROUP14
diffie-hellman-group14-sha256 (HW-accelerated MPI on ESP32)
SshPhase
SSH connection lifecycle phase.
@ SSH_PHASE_KEXINIT
Awaiting the client KEXINIT.
@ SSH_PHASE_AUTH
User authentication in progress (RFC 4252).
@ SSH_PHASE_SERVICE
Awaiting SERVICE_REQUEST ("ssh-userauth").
@ SSH_PHASE_OPEN
Authenticated; connection/channel protocol active.
@ SSH_PHASE_NEWKEYS
Awaiting SSH_MSG_NEWKEYS.
@ SSH_PHASE_BANNER
Awaiting the client identification string.
@ SSH_PHASE_DH_INIT
Awaiting SSH_MSG_KEXDH_INIT.
bool ssh_rekey_due(uint32_t seq_send, uint32_t seq_recv, uint32_t elapsed_ms, uint32_t pkt_threshold, uint32_t time_threshold_ms)
Pure re-key decision (RFC 4253 §9: "after each gigabyte ... or after each hour").
int ssh_kexdh_build_reply(const uint8_t *ks, size_t ks_len, const uint8_t *f_be, const uint8_t *sig, size_t sig_len, uint8_t *out, size_t *out_len, size_t cap)
Build SSH_MSG_KEXDH_REPLY (RFC 4253 §8, RFC 8332 §3).
SshSession ssh_sess[MAX_SSH_CONNS]
Static pool of SSH session state (BSS), one per SSH slot.
void ssh_kex_set_prefer_rsa(bool prefer)
Steer KEX / host-key negotiation toward RSA + DH-group14 (default) or toward the modern curve25519 + ...
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).
void ssh_transport_init(uint8_t i)
Reset transport state for slot i to the start of a handshake.
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.
int ssh_kex_exchange_hash(uint8_t i, const uint8_t *e_be, const uint8_t *f_be, const uint8_t *k_be, const uint8_t *ks, size_t ks_len, uint8_t out[SSH_SHA256_DIGEST_LEN])
Compute the SSH exchange hash H (RFC 4253 §8).
int ssh_transport_server_banner(uint8_t *out, size_t *out_len, size_t cap)
Write the server identification string ("SSH-2.0-…\r\n") to out.
int ssh_kexdh_parse_init(const uint8_t *payload, size_t len, uint8_t e_be[256])
Parse SSH_MSG_KEXDH_INIT, extracting the client DH value e.
bool ext_info_c
Client advertised ext-info-c (RFC 8308): send EXT_INFO.
uint8_t banner_buf[SSH_VERSION_MAX]
Accumulator for the inbound banner.
SshKexAlg kex_alg
negotiated in KEXINIT.
uint8_t ecdh_pk[32]
Server X25519 ephemeral public (curve25519 KEX only).
SshHostkeyAlg hostkey_alg
negotiated in KEXINIT.
uint16_t i_c_len
Length of i_c.
uint8_t session_id[SSH_SHA256_DIGEST_LEN]
H from the first KEX (RFC 4253 §7.2).
bool have_session_id
True once the first KEX completes.
SshPhase phase
Current handshake phase.
char v_c[SSH_VERSION_MAX]
Client identification string (no CR LF).
uint16_t i_s_len
Length of i_s.
bool authed
True after successful user authentication.
uint8_t mac_alg
SSH_MAC_* negotiated in KEXINIT (aes cipher only; 0 = hmac-sha2-256).
uint16_t banner_len
Bytes buffered in banner_buf.
uint8_t i_c[SSH_KEXINIT_MAX]
Client KEXINIT payload (for H).
uint32_t last_kex_ms
detws_millis() when the last KEX completed (server-initiated re-key timer).
uint8_t auth_failures
Failed USERAUTH_REQUESTs (brute-force limit, RFC 4252 §4).
uint8_t i_s[SSH_KEXINIT_S_MAX]
Server KEXINIT payload (for H).
uint8_t ecdh_sk[32]
Server X25519 ephemeral private (curve25519 KEX only; wiped after).
uint8_t cipher_alg
SSH_CIPHER_* negotiated in KEXINIT (0 = aes256-ctr).
uint16_t v_c_len
Length of v_c.