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