ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
transport.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file transport.h
6 * @brief RFC 4253 transport layer: identification exchange, algorithm negotiation, key exchange.
7 */
8
9#ifndef PROTOCORE_TRANSPORT_TRANSPORT_H
10#define PROTOCORE_TRANSPORT_TRANSPORT_H
11
16#include "crypto/hash/sha256/sha256.h" // PROTOCORE_SHA256_DIGEST_LEN - the exchange hash and session id
17#include "locus_carcerum/locus_carcerum.h" // mmgr_zero_buf (the canonical secure wipe)
20
21#include "protocore_config.h"
22
24
25/**
26 * @brief One direction's codec state, handed to the packet layer per call.
27 *
28 * RFC 4253 sec 6 sits under sec 7, so the codec is told which keys to use rather than reading the
29 * session that negotiated them. Each direction switches on its own SSH_MSG_NEWKEYS (sec 7.3).
30 */
31typedef struct
32{
33 proto_bool enc; ///< that direction's cipher/MAC is active
34 uint8_t epoch; ///< key epoch it reads out of ssh_keys[slot][]
35} SshDir;
36
37/** @brief Negotiated key-exchange method. */
39{
40 SSH_KEX_DH_GROUP14 = 0, ///< diffie-hellman-group14-sha256 (RFC 8268)
41 SSH_KEX_CURVE25519 = 1, ///< curve25519-sha256 (RFC 8731)
42 SSH_KEX_MLKEM768_X25519 = 2, ///< mlkem768x25519-sha256
43 SSH_KEX_ECDH_NISTP256 = 3, ///< ecdh-sha2-nistp256 (RFC 5656 sec 4)
44 SSH_KEX_SNTRUP761_X25519 = 4 ///< sntrup761x25519-sha512@openssh.com
46
47/** @brief Negotiated host-key / signature algorithm. */
49{
50 SSH_HOSTKEY_RSA_SHA256 = 0, ///< rsa-sha2-256 (RFC 8332)
51 SSH_HOSTKEY_ED25519 = 1, ///< ssh-ed25519 (RFC 8709)
52 SSH_HOSTKEY_RSA_SHA512 = 2, ///< rsa-sha2-512 (RFC 8332)
53 SSH_HOSTKEY_ECDSA_NISTP256 = 3 ///< ecdsa-sha2-nistp256 (RFC 5656)
55
56/**
57 * @brief SSH transport/session state for one connection (BSS pool).
58 *
59 * Holds the handshake phase plus the values that must persist across messages to compute the
60 * exchange hash H: the identification strings (V_C, V_S) and the two KEXINIT payloads (I_C, I_S).
61 * The exchange hash from the first KEX is retained as the session id.
62 */
63typedef struct
64{
65 SshPhase phase; ///< Current handshake phase.
66
67 SshKexAlg kex_alg; ///< negotiated in KEXINIT.
68 SshHostkeyAlg hostkey_alg; ///< negotiated in KEXINIT.
69 // RFC 4253 sec 7.1 negotiates each direction's cipher and MAC from its own name-list, so the two
70 // directions may differ and are kept apart from KEXINIT through to key install.
71 uint8_t cipher_alg_c2s; ///< SSH_CIPHER_* client-to-server.
72 uint8_t cipher_alg_s2c; ///< SSH_CIPHER_* server-to-client.
73 uint8_t mac_alg_c2s; ///< SSH_MAC_* client-to-server (aes cipher only; 0 = hmac-sha2-256).
74 uint8_t mac_alg_s2c; ///< SSH_MAC_* server-to-client (aes cipher only; 0 = hmac-sha2-256).
75
76 // Every buffer below is the constants region of the connection's span, at its own named offset.
77 // Null until the connection claims the slot and splits that borrow.
78 uint8_t *ecdh_sk; ///< 32B: X25519 scalar / P-256 d, ephemeral private. Wiped by ssh_dh_wipe().
79 uint8_t *ecdh_pk; ///< 32B: X25519 ephemeral public (curve25519 KEX only).
80
81 char *v_c; ///< SSH_VERSION_MAX: client identification string (no CR LF).
82 uint16_t v_c_len; ///< Length of v_c.
83 char *v_s; ///< SSH_VERSION_MAX: server identification string (no CR LF).
84 uint16_t v_s_len; ///< Length of v_s.
85
86 uint8_t *ident_buf; ///< SSH_VERSION_MAX: accumulator for the inbound identification string.
87 uint16_t ident_len; ///< Bytes buffered in ident_buf.
88
89 uint8_t *i_c; ///< PROTOCORE_SSH_I_C_MAX: client KEXINIT payload (for H).
90 uint16_t i_c_len; ///< Length of i_c.
91 uint8_t *i_s; ///< PROTOCORE_SSH_I_S_MAX: server KEXINIT payload (for H).
92 uint16_t i_s_len; ///< Length of i_s.
93
94 uint8_t *cpub; ///< PROTOCORE_SSH_CPUB_MAX: exchange value the client sent - e, Q_C or C_INIT (for H).
95 uint16_t cpub_len; ///< Length of cpub.
96
97 uint8_t *session_id; ///< SSH_KEXHASH_MAX_LEN: H from the first KEX (RFC 4253 sec 7.2).
98 uint8_t session_id_len; ///< Session id length (the first KEX's exchange-hash length).
99 proto_bool have_session_id; ///< True once the first KEX completes.
100
101 // RFC 4253 sec 6 is a layer under sec 7, so the codec is handed one of these per call rather
102 // than reading them. Each switches on its own SSH_MSG_NEWKEYS (sec 7.3).
103 SshDir out; ///< Our outbound direction: encrypted once we sent NEWKEYS, and the epoch it reads.
104 SshDir in; ///< Our inbound direction: encrypted once the peer's arrives.
105
106 proto_bool kex_active; ///< An exchange is running, from KEXINIT to NEWKEYS (sec 9).
107 // sec 7.1 restricts what may be sent from the moment THIS end sends its KEXINIT until it sends
108 // its NEWKEYS, which is a narrower window than kex_active: an exchange is already running while
109 // this end is still waiting to send its own KEXINIT, and that first one must not be refused.
110 proto_bool kexinit_sent; ///< This end has sent its KEXINIT and not yet its NEWKEYS (sec 7.1).
111 // sec 9: "the state of the higher-level protocols is not affected by the key exchange", so a
112 // re-exchange has to put the connection back where it interrupted it. The phase alone cannot say
113 // where that was - every exchange ends in SSH_PHASE_NEWKEYS however it started.
114 SshPhase phase_before_kex; ///< What to resume when this exchange completes (sec 9).
115 proto_bool drop_guessed_kex_pkt; ///< The peer guessed a KEX that lost negotiation (sec 7.1).
116 proto_bool ext_info_enabled; ///< Peer offered its role's RFC 8308 sec 2.2 indicator.
117 proto_bool ext_info_sent; ///< EXT_INFO already went out; RFC 8308 sec 2.4 allows it once.
118 proto_bool authed; ///< True after successful user authentication.
119 uint32_t last_kex_ms; ///< protocore_millis() when the last KEX completed.
120} SshSession;
121
122/** @brief Static pool of SSH session state (BSS), one per SSH slot. */
124
125/**
126 * @brief Steer KEX and host-key negotiation toward RSA with DH-group14, or toward curve25519 with
127 * ed25519.
128 *
129 * Both suites are advertised whatever this is set to; it orders them, so a peer that supports only
130 * one still connects. Runtime-selectable, before the handshake.
131 */
133
134/** @brief Current negotiation preference (true = prefer RSA / DH). */
136
137/**
138 * @brief The session identifier for slot @p i, or null before the first key exchange completes.
139 *
140 * RFC 4253 sec 10: "When the service starts, it may have access to the session identifier generated
141 * during the key exchange." It is the first exchange's hash H (sec 7.2) and does not change on a
142 * re-exchange, so a service that binds to it stays bound.
143 *
144 * @param i SSH slot index.
145 * @param len Set to the identifier's length: 32 for the SHA-256 methods, 64 for the SHA-512 one.
146 */
147const uint8_t *ssh_session_id(uint8_t i, size_t *len);
148
149/**
150 * @brief Latch the first exchange's hash as slot @p i's session identifier (RFC 4253 sec 7.2).
151 *
152 * "The exchange hash H from the first key exchange is additionally used as the session identifier."
153 * Both roles compute H in their own half of sec 8 and hand it here; the second and later exchanges
154 * are ignored, so the identifier never moves under a service that bound to it.
155 */
156void ssh_session_id_latch(uint8_t i, const uint8_t *h, size_t h_len);
157
158/** @brief True when @p a hashes with SHA-512 rather than SHA-256 (RFC 8268, RFC 8731). */
160
161/**
162 * @brief Verify the server's signature over the exchange hash with its host key (RFC 4253 sec 8).
163 *
164 * @param i the SSH slot the exchange belongs to
165 * @param ks the host key blob the server sent
166 * @param ks_len its length
167 * @param sig the signature blob
168 * @param sig_len its length
169 * @param h the exchange hash the signature is taken over
170 * @param h_len its length
171 * @return true when the signature checks out under the blob's own algorithm.
172 */
173proto_bool ssh_hostkey_verify(uint8_t i, const uint8_t *ks, size_t ks_len, const uint8_t *sig, size_t sig_len,
174 const uint8_t *h, size_t h_len);
175
176/** @brief RFC 4253 sec 6 binary packet: the bytes a receive consumes, and the body it carries. */
177typedef struct
178{
179 const uint8_t *data; ///< bytes a receive consumes
180 const uint8_t *payload; ///< a KEXINIT or KEXDH payload
181 size_t len; ///< how many
182 size_t consumed; ///< bytes a receive took from data
184
185/** @brief Where a build or a send writes, and what it wrote. */
186typedef struct
187{
188 uint8_t *out; ///< where a build or a send writes
189 size_t out_len; ///< what it wrote
190 size_t cap; ///< how much room it has
192
193/** @brief RFC 4253 sec 8: every term the exchange hash H is taken over, and where H lands. */
194typedef struct
195{
196 proto_bool pub_is_string; ///< the peer public value is a string, not an mpint
197 const uint8_t *cpub; ///< the client public value
198 size_t cpub_len; ///< its length
199 const uint8_t *spub; ///< the server public value
200 size_t spub_len; ///< its length
201 const uint8_t *k_be; ///< the shared secret, big-endian
202 size_t k_len; ///< its length
203 const uint8_t *ks; ///< the host key blob
204 size_t ks_len; ///< its length
205 proto_bool k_is_string; ///< that secret is a string, not an mpint
206 proto_bool is512; ///< the method hashes with SHA-512
207 uint8_t hash[SSH_KEXHASH_MAX_LEN]; ///< where the exchange hash H lands
208 size_t hash_len; ///< its length: 32 for the SHA-256 methods, 64 for the SHA-512 one
210
211/** @brief RFC 4253 sec 9 key re-exchange: what has passed since the last one, against its budget. */
212typedef struct
213{
214 uint32_t seq_send; ///< packets sent since the last exchange
215 uint32_t seq_recv; ///< packets received since it
216 uint32_t elapsed_ms; ///< time since it
217 uint32_t pkt_threshold; ///< the volume budget
218 uint32_t time_threshold_ms; ///< the time budget
220
221/**
222 * @brief The RFC 4253 transport state machine, as the operations both roles consume.
223 *
224 * One member per rule the RFC states, so a section number resolves to exactly one implementation.
225 * Role selects which half of a mirrored pair a caller sends - it does not select a second machine.
226 *
227 * @var SshTransportNs::recv_ident sec 4.2 receive the peer identification string
228 * @var SshTransportNs::send_ident sec 4.2 send ours
229 * @var SshTransportNs::kexinit_build sec 7.1 build our KEXINIT, keeping a copy for H
230 * @var SshTransportNs::kexinit_parse sec 7.1 parse the peer's and negotiate every list
231 * @var SshTransportNs::kex_generate sec 8 generate this exchange's ephemeral
232 * @var SshTransportNs::exchange_hash sec 8 the exchange hash H over the negotiated method's terms
233 * @var SshTransportNs::kexdh_reply sec 8 the KEXDH half this role sends
234 * @var SshTransportNs::newkeys_sent sec 7.3 our outbound switches when we send NEWKEYS
235 * @var SshTransportNs::newkeys_complete sec 7.3 our inbound switches when the peer's arrives
236 * @var SshTransportNs::rekey_due sec 9 the volume and time budget since the last exchange
237 * @var SshTransportNs::begin_rekey sec 9 start a re-exchange, when not already doing one
238 *
239 * A caller sets the members a call takes, invokes it through ::SshTransport, and reads the outcome
240 * off the same handle.
241 *
242 * @var SshTransportNs::slot the SSH slot a call acts on
243 * @var SshTransportNs::pkt sec 6 the bytes one message occupies
244 * @var SshTransportNs::out_args where a build or a send writes
245 * @var SshTransportNs::kexhash sec 8 the terms the exchange hash H is taken over
246 * @var SshTransportNs::rekey sec 9 the volume and time budget since the last exchange
247 * @var SshTransportNs::ok a call's true/false outcome
248 * @var SshTransportNs::i32 a call's signed outcome
249 */
250
251typedef struct
252{
253 uint8_t slot; ///< the SSH slot a call acts on
254 SshPacketArgs pkt; ///< sec 6 the bytes one message occupies
255 SshTransportOut out_args; ///< where a build or a send writes
256 SshKexHashArgs kexhash; ///< sec 8 the terms the exchange hash H is taken over
257 SshRekeyArgs rekey; ///< sec 9 the volume and time budget since the last exchange
259 int i32;
261
262/** @brief The operands and the outcome. */
264
265/** @brief The entries. */
266typedef struct
267{
268 void (*const recv_ident)(uint8_t *work);
269 void (*const send_ident)(uint8_t *work);
270 void (*const kexinit_build)(uint8_t *work);
271 void (*const kexinit_parse)(uint8_t *work);
272 void (*const kex_generate)(uint8_t *work);
273 void (*const exchange_hash)(uint8_t *work);
274 void (*const kexdh_reply)(uint8_t *work);
275 void (*const newkeys_sent)(uint8_t *work);
276 void (*const newkeys_complete)(uint8_t *work);
277 void (*const rekey_due)(uint8_t *work);
278 void (*const begin_rekey)(uint8_t *work);
280
281// What the table binds, defined once in the .c and taking one parameter each: everything
282// else an entry needs is an operand in SshTransportV or a region of the borrow at a fixed offset.
294
295// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
296// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
297// `SshTransport.recv_ident(work)` resolves to a named function and becomes a DIRECT call. An extern table
298// leaves the call indirect and the symbol live at every level, -O2 -flto included.
299static const SshTransportNs SshTransport __attribute__((unused)) = {
311};
312
313/**
314 * @brief The PROTOCORE_SSH_TRANSPORT_BORROW bytes this module's state lives in.
315 *
316 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
317 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
318 * walks, so the state lasts the life of the program.
319 *
320 * @return the span.
321 */
323
324/** @brief Reader shorthand: SSH_TRANSPORT->kexinit_parse(...). */
325#define SSH_TRANSPORT (&SshTransport)
326
327#if PROTOCORE_SSH_KEX_BENCH
328// Wall-clock KEX bench (perf / FEATURE_PERFORMANCE): one owned context holding the two device-side compute
329// spans of a key exchange, in microseconds. ssh_kex_generate records the ephemeral-keygen span (one X25519
330// base multiply for a curve25519 KEX) into last_kexgen_us; ssh_kexdh_handle records the reply span
331// (shared-secret X25519 + host-key sign + exchange hash + KDF + reply assembly) into last_kexreply_us and
332// bumps kex_count. The rig firmware watches kex_count and prints both over its own serial - src writes and
333// output. Compiled out entirely unless PROTOCORE_SSH_KEX_BENCH is defined (a rig-only measurement build).
334typedef struct
335{
336 volatile long long last_kexgen_us; ///< ssh_kex_generate: ephemeral X25519 base-multiply span.
337 volatile long long last_kexreply_us; ///< ssh_kexdh_handle: reply span (shared secret + sign + hash + KDF).
338 volatile unsigned kex_count; ///< bumped after each completed KEX; the rig prints on change.
339} SshKexBenchCtx;
340extern SshKexBenchCtx protocore_ssh_kex_bench;
341#endif
342
343/** @brief Max bytes ssh_kdf_derive() can produce (4 SHA-256 blocks). */
344#define SSH_KDF_MAX (4 * PROTOCORE_SHA256_DIGEST_LEN)
345
346/**
347 * @brief One key exchange's derivation inputs, passed by reference.
348 *
349 * Every value here is fixed for the whole of a KEX and read by each of the six derivations, so it
350 * travels as one pointer: an argument list this wide spills past the register window and pays for it
351 * on every call, at the deepest call depth in the library.
352 */
353typedef struct
354{
355 uint8_t *work; ///< PROTOCORE_SSH_KDF_BORROW bytes: the hash context, then the K1 || K2 chain.
356 const uint8_t *K_be; ///< Shared secret K, big-endian, 256 bytes.
357 const uint8_t *H; ///< Current exchange hash.
358 const uint8_t *session_id; ///< H of the first KEX; equals H until the first re-key.
359 size_t h_len; ///< Length of H.
360 size_t sid_len; ///< Length of session_id.
361 proto_bool k_is_string; ///< Encode K as a plain SSH string (hybrid KEX), not an mpint.
362 proto_bool is512; ///< Hash with SHA-512 instead of SHA-256.
364
365// ---------------------------------------------------------------------------
366// Packet state per connection
367// ---------------------------------------------------------------------------
368
369/**
370 * @brief Per-connection SSH binary packet state.
371 *
372 * Allocated in ssh_pool[] (BSS); one entry per SSH connection slot.
373 * Key material is in ssh_keys[] - a separate BSS symbol to prevent linear
374 * overflow from packet buffers into key material.
375 */
376typedef struct
377{
378 // RFC 4253 sec 6.4: incremented per packet and never reset, not even across a re-exchange, so
379 // these stay with the codec rather than moving to the session with the protocol flags.
380 uint32_t seq_no_send; ///< Outgoing sequence number.
381 uint32_t seq_no_recv; ///< Incoming sequence number.
382
383 // SSH keys are named by direction (client->server "c2s", server->client "s2c"), fixed by RFC 4253
384 // §7.2 regardless of role. A server sends s2c / receives c2s; a client is the mirror. This flag
385 // selects the direction at each cipher/MAC site so one packet implementation serves both roles.
386 // Default false = server (so existing server code is unchanged); ssh_pkt_set_client() flips it.
388
389 // Receive reassembly: we may receive partial packets across TCP segments. rx_buf is the whole of
390 // the data packet region of the connection's span, at its own named offset.
391 uint8_t *rx_buf; ///< SSH_RX_ASM_CAP bytes at SSH_OFF_RX_ASM. Null until claimed.
392 size_t rx_len; ///< Bytes currently in rx_buf.
393
394 // One finished packet waiting for a worker to put it on the wire. The codec frames into
395 // tx_wire and raises tx_ready; the worker sends from tx_off and lowers the flag when the last
396 // byte is out. The codec never reaches the wire itself.
397 //
398 // tx_wire is taken once from the secure pool's persistent end - the end no mark walks - and
399 // reused for every packet on this slot. Releasing per packet would wipe the whole wire buffer
400 // each time, and the mark end cannot hold it anyway: mark/release is a bump discipline, so one
401 // slot's release would reclaim another slot's borrow.
402 uint8_t *tx_wire; ///< The wire buffer for this slot. Null until the first packet.
403 size_t tx_len; ///< Bytes of the framed packet.
404 size_t tx_off; ///< Bytes already put on the wire.
405 proto_bool tx_ready; ///< A packet is framed and waiting for a worker.
406
407 // The packet MAC and the key exchange work out of these, and they come from the same one borrow as
408 // tx_wire. Held for the slot's life so neither costs a borrow or a wipe on the packet path.
409 uint8_t *mac_work; ///< PROTOCORE_HMAC_SHA256_BORROW bytes. Null until the first packet.
410 // SSH_CIPHER_WORK_LEN bytes for the negotiated record cipher, its own region so a MAC and a
411 // cipher on the same packet never reach each other's bytes.
412 uint8_t *cipher_work;
413 // PROTOCORE_CRYPTO_BORROW_MAX bytes for the handshake's crypto: the exchange hash, the RFC 4253 sec 7.2 KDF,
414 // the host-key signature and the userauth one. Those run in sequence, never at once, so they share.
415 uint8_t *crypto_work;
417
418/** @brief Static packet state pool (BSS). One entry per SSH slot. */
420
421/**
422 * @brief Initialize the packet state for SSH connection slot @p i.
423 *
424 * Zeroes the sequence numbers and the transmit state, keeping the slot's storage pointers. The
425 * protocol flags are the session's (ssh_transport.h) and are reset by ssh_transport_init().
426 *
427 * @param i SSH slot index.
428 */
429void ssh_pkt_init(uint8_t i);
430
431/**
432 * @brief Bind the session state for SSH connection slot @p i to the slot's storage.
433 *
434 * Zeroes the session, then points each of its buffers and both key epochs at their offsets within
435 * the slot, leaves the phase at SSH_PHASE_IDENT with the first key exchange already running, and
436 * marks both epochs inactive.
437 *
438 * @param i SSH slot index.
439 */
440void ssh_transport_init(uint8_t i);
441
442/**
443 * @brief Take the slot's one persistent borrow if it has none yet, and split it.
444 *
445 * Sets @ref SshPacketState::tx_wire, @ref SshPacketState::mac_work and @ref SshPacketState::crypto_work.
446 * Idempotent, and false only when the pool cannot cover the slot.
447 */
449
450/**
451 * @brief Mark slot @p i as the SSH client role (call once, right after ssh_pkt_init).
452 *
453 * Flips the send/receive key direction: the client encrypts with the c2s key set and decrypts with
454 * the s2c one, the mirror of the server. Without this a slot defaults to the server role.
455 */
456void ssh_pkt_set_client(uint8_t i);
457
458/**
459 * @brief Frame the @p payload_len bytes already written at @p wire + @ref SSH_WIRE_PAYLOAD_OFF.
460 *
461 * The in-place form of ssh_pkt_send(): same framing, padding, encryption and MAC, over a payload the
462 * caller has already placed. @p wire holds the finished packet on return.
463 *
464 * @return 0 on success, -1 on overflow or sequence-number exhaustion.
465 */
466int ssh_pkt_send_at(uint8_t i, uint8_t *wire, size_t payload_len, size_t *out_len, size_t wire_cap, const SshDir *dir);
467
468/**
469 * @brief Frame @p payload for slot @p i into the secure pool and raise the flag a worker drains.
470 *
471 * Borrows the wire buffer itself, for a caller that already holds its message somewhere else -
472 * handshake and control traffic, which is small and infrequent. On return the packet is framed and
473 * @ref SshPacketState::tx_ready is set; a worker puts the bytes on the wire and releases the
474 * borrow. This layer never reaches the wire.
475 *
476 * @return 0 on success, -1 if a packet is already pending, the pool is exhausted, or framing fails.
477 */
478int ssh_pkt_emit(uint8_t i, const uint8_t *payload, size_t len, const SshDir *dir);
479
480/**
481 * @brief Build and send one SSH binary packet.
482 *
483 * Frames @p payload according to RFC 4253 §6:
484 * - Adds random padding to align to 16-byte boundary.
485 * - If encrypted: encrypts with AES-256-CTR, appends HMAC-SHA2-256 MAC.
486 * - Increments seq_no_send; closes connection if threshold reached.
487 *
488 * The serialized packet is written into @p out. *@p out_len is set to the
489 * number of bytes written. @p out must be at least
490 * (4 + 1 + payload_len + 16 + 32) bytes.
491 *
492 * @param i SSH slot index.
493 * @param payload Plaintext SSH message payload.
494 * @param payload_len Length of @p payload.
495 * @param out Output buffer for the wire packet.
496 * @param out_len Set to the number of bytes written into @p out.
497 * @param out_cap Capacity of @p out.
498 * @return 0 on success, -1 on overflow or sequence-number exhaustion.
499 */
500int ssh_pkt_send(uint8_t i, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len, size_t out_cap,
501 const SshDir *dir);
502
503/**
504 * @brief Callback invoked once per complete, verified inbound SSH message.
505 *
506 * @param slot SSH slot index.
507 * @param msg_type First payload byte (SSH message number).
508 * @param payload Decrypted message payload (includes @p msg_type at [0]).
509 * @param payload_len Length of @p payload.
510 */
511typedef void (*ssh_msg_handler_t)(uint8_t slot, uint8_t msg_type, const uint8_t *payload, size_t payload_len);
512
513/**
514 * @brief Receive and process one or more SSH binary packets from @p data.
515 *
516 * Appends @p len bytes from @p data to the receive buffer for slot @p i,
517 * then extracts complete packets. For each complete packet:
518 * - If encrypted: decrypts with AES-256-CTR, verifies HMAC-SHA2-256.
519 * Closes connection (returns -1) on MAC failure without processing payload.
520 * - Increments seq_no_recv; closes connection if threshold reached.
521 * - Calls @p handler(slot, msg_type, payload, payload_len) for the payload.
522 *
523 * @param i SSH slot index.
524 * @param data Received bytes (from TCP).
525 * @param len Number of bytes in @p data.
526 * @param handler Callback invoked once per complete, verified packet.
527 * @return 0 on success, -1 on MAC failure or sequence-number exhaustion
528 * (caller must close the TCP connection).
529 */
530int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler, const SshDir *dir);
531
532/**
533 * @brief Send SSH_MSG_DISCONNECT with reason @p reason_code.
534 *
535 * Sends the packet, then zeroes the packet state and key material for slot @p i.
536 *
537 * @param i SSH slot index.
538 * @param reason_code One of SSH_DISCONNECT_* constants.
539 * @param out Output buffer for the wire packet.
540 * @param out_len Set to the number of bytes written.
541 * @param out_cap Capacity of @p out.
542 * @return 0 on success, -1 on error.
543 */
544int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap,
545 const SshDir *dir);
546
547// ---------------------------------------------------------------------------
548// RFC 4253 sec 11.4 - Reserved Messages
549// ---------------------------------------------------------------------------
550
551/** @brief Bytes in an SSH_MSG_UNIMPLEMENTED payload: the message number and one uint32. */
552#define SSH_UNIMPLEMENTED_LEN 5u
553
554/**
555 * @brief Build the SSH_MSG_UNIMPLEMENTED payload answering the packet slot @p i last received.
556 *
557 * "An implementation MUST respond to all unrecognized messages with an SSH_MSG_UNIMPLEMENTED
558 * message in the order in which the messages were received." The sequence number it carries is
559 * the receive counter's, which this layer owns; the caller frames and sends the payload.
560 *
561 * @param i SSH slot index.
562 * @param out Output buffer for the payload.
563 * @param out_len Set to SSH_UNIMPLEMENTED_LEN.
564 * @param out_cap Capacity of @p out.
565 * @return 0 on success, -1 on a bad slot or too small a buffer.
566 */
567int ssh_pkt_unimplemented(uint8_t i, uint8_t *out, size_t *out_len, size_t out_cap);
568
569// ---------------------------------------------------------------------------
570// RFC 4253 sec 7.2 - key material
571// ---------------------------------------------------------------------------
572
573// These two are anonymous enums: the constants are what the code names, and the negotiated value
574// travels as a uint8_t through ssh_kex_install_keys, ssh_mac_is_etm, ssh_mac_len, and the
575// per-direction cipher_mode_* / mac_mode_* session fields.
576
577/** @brief Negotiated bulk cipher for a session. */
578enum
579{
580 SSH_CIPHER_AES256CTR = 0, ///< aes256-ctr + a separate HMAC (the fallback)
581 SSH_CIPHER_CHACHA20POLY1305 = 1, ///< chacha20-poly1305@openssh.com (AEAD; no separate MAC)
582 SSH_CIPHER_AES256GCM = 2, ///< aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
583};
584
585/** @brief Negotiated MAC for the aes256-ctr cipher (unused with the chacha AEAD). */
586enum
587{
588 SSH_MAC_HMAC_SHA256 = 0, ///< hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
589 SSH_MAC_HMAC_SHA512 = 1, ///< hmac-sha2-512 (encrypt-and-MAC)
590 SSH_MAC_HMAC_SHA256_ETM = 2, ///< hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
591 SSH_MAC_HMAC_SHA512_ETM = 3, ///< hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
592};
593
594/** @brief True if @p mac_mode is an encrypt-then-MAC variant (length in the clear, MAC over ciphertext). */
595static inline proto_bool ssh_mac_is_etm(uint8_t mac_mode)
596{
597 return mac_mode == SSH_MAC_HMAC_SHA256_ETM || mac_mode == SSH_MAC_HMAC_SHA512_ETM;
598}
599/** @brief MAC tag / key length in bytes for @p mac_mode (32 for SHA-256, 64 for SHA-512). */
600static inline uint8_t ssh_mac_len(uint8_t mac_mode)
601{
602 return (mac_mode == SSH_MAC_HMAC_SHA512 || mac_mode == SSH_MAC_HMAC_SHA512_ETM) ? 64 : 32;
603}
604
605// Secure wipe: the canonical mmgr_zero_buf() lives in locus_carcerum/locus_carcerum.h (included above). Use it for
606// any buffer that held key material - a volatile store the compiler may not elide, unlike a dead memset.
607
608// ---------------------------------------------------------------------------
609// Session key material (one entry per SSH connection)
610// ---------------------------------------------------------------------------
611
612/**
613 * @brief AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
614 *
615 * This struct occupies a separate BSS symbol (ssh_keys[]) from the packet
616 * receive buffer (ssh_pool[].pkt_buf). See the security model at the top
617 * of this file for why that separation matters.
618 *
619 * Key derivation follows RFC 4253 §7.2. After the DH exchange hash H is
620 * known and K is available, six values are derived:
621 *
622 * IV_c2s = SHA256(K || H || "A" || session_id) [16 bytes]
623 * IV_s2c = SHA256(K || H || "B" || session_id) [16 bytes]
624 * key_c2s = SHA256(K || H || "C" || session_id) [32 bytes]
625 * key_s2c = SHA256(K || H || "D" || session_id) [32 bytes]
626 * mac_c2s = SHA256(K || H || "E" || session_id) [32 bytes]
627 * mac_s2c = SHA256(K || H || "F" || session_id) [32 bytes]
628 *
629 * aes_key/aes_ctr C→S are the client-to-server key + counter (server decrypts inbound); S→C are the
630 * reverse (server encrypts outbound).
631 */
632typedef struct
633{
634 // aes256-ctr stores the raw 32-byte key and the 16-byte IV per direction and rebuilds its key schedule
635 // per packet in the shared crypto scratch, so no expanded CTR key lingers here. The IV is the running
636 // 128-bit counter (see aes256ctr.h).
637 //
638 // aes256-gcm@openssh.com shares aes_iv_* (the modes are mutually exclusive) but NOT aes_key_* - see the
639 // keyed contexts below.
640 // Every key buffer below is one epoch of the kmt region of the connection's span, at its own
641 // named offset. Null until the connection claims the slot and splits its borrow.
642 uint8_t *aes_key_c2s; ///< PROTOCORE_AES256CTR_KEY_LEN: AES key C→S (server decrypts inbound).
643 uint8_t *aes_key_s2c; ///< PROTOCORE_AES256CTR_KEY_LEN: AES key S→C (server encrypts outbound).
644 uint8_t *aes_iv_c2s; ///< PROTOCORE_AES256CTR_CTR_LEN: AES IV C→S (CTR counter / GCM nonce); advances per packet.
645 uint8_t *aes_iv_s2c; ///< PROTOCORE_AES256CTR_CTR_LEN: AES IV S→C (CTR counter / GCM nonce); advances per packet.
646
647 uint8_t *mac_key_c2s; ///< 64B: HMAC key, client-to-server (aes mode); 32 bytes for SHA-256, 64 for SHA-512.
648 uint8_t *mac_key_s2c; ///< 64B: HMAC key, server-to-client (aes mode).
649 // RFC 4253 sec 7.1 negotiates the cipher and the MAC per direction, so each is stored per direction
650 // and a session may run different ones each way (0 = aes256-ctr / hmac-sha2-256 E&M).
651 uint8_t mac_mode_c2s; ///< SSH_MAC_* client-to-server (aes256-ctr only).
652 uint8_t mac_mode_s2c; ///< SSH_MAC_* server-to-client (aes256-ctr only).
653 uint8_t cipher_mode_c2s; ///< SSH_CIPHER_* client-to-server.
654 uint8_t cipher_mode_s2c; ///< SSH_CIPHER_* server-to-client.
655 // chacha20-poly1305@openssh.com: 512-bit key per direction (K_main || K_header); no IV, no MAC key.
656 uint8_t *chacha_key_c2s; ///< PROTOCORE_CHACHAPOLY_KEY_LEN: client-to-server, used only in chacha mode.
657 uint8_t *chacha_key_s2c; ///< PROTOCORE_CHACHAPOLY_KEY_LEN: server-to-client, used only in chacha mode.
658
659 // aes256-gcm@openssh.com (RFC 5647) reuses aes_iv_* above (mode-exclusive with CTR): the low 12 bytes
660 // are the nonce, advanced per packet by AesGcm.iv_increment. No separate MAC key.
661 //
662 // It does NOT use aes_key_*. A GCM key becomes a keyed context at install time and the raw key is
663 // wiped there, so in this mode the expanded schedule is the only key material resident - strictly
664 // less than CTR mode keeps. The context stays for the life of the key because standing one up costs
665 // ~9,200 cycles on an ESP32-S3, a fixed price per packet that dominates small interactive traffic
666 // (see aesgcm.h). Wiped on rekey and by ssh_keymat_wipe() on close.
667 // These two open the kmt epoch, so the region's 8-alignment is the epoch's own start.
668 uint8_t *gcm_ctx_c2s; ///< PROTOCORE_AESGCM_BORROW: keyed GCM context C→S (server opens inbound).
669 uint8_t *gcm_ctx_s2c; ///< PROTOCORE_AESGCM_BORROW: keyed GCM context S→C (server seals outbound).
670
671 proto_bool active; ///< True once keys are installed after successful KEX.
672} SshKeyMat;
673
674/**
675 * @brief Pool of session key material, two epochs per MAX_SSH_CONNS.
676 *
677 * RFC 4253 sec 7.3 switches each direction on its own NEWKEYS, so a re-key derives into the epoch
678 * neither direction is reading and each direction moves to it when its NEWKEYS crosses.
679 * The SshDir the codec is handed selects which one that site reads.
680 * Zeroed on connection close by ssh_keymat_wipe(slot).
681 */
683
684// ---------------------------------------------------------------------------
685// DH ephemeral state (one entry per SSH connection, zeroed after KEX)
686// ---------------------------------------------------------------------------
687
688/**
689 * @brief Ephemeral Diffie-Hellman state for one SSH connection.
690 *
691 * The three protocore_bignum fields (y, f, K) together hold 768 bytes of sensitive
692 * material. The entire struct is wiped by ssh_dh_wipe() immediately after
693 * session keys are derived from K.
694 *
695 * FIELD LIFETIME:
696 * y - generated by ssh_dh_generate(); zeroed in ssh_dh_wipe().
697 * f - computed in ssh_dh_generate() as g^y mod p; sent in KEXDH_REPLY;
698 * zeroed in ssh_dh_wipe().
699 * K - computed in ssh_dh_finish() as e^y mod p; used for key derivation;
700 * zeroed in ssh_dh_wipe() AFTER keys are installed.
701 */
702// All three sit in the kmt region of the connection's span, each at its own named offset, after the
703// two key epochs. Null until the connection claims the slot and splits its borrow.
704typedef struct
705{
706 protocore_bignum *y; ///< Server ephemeral private DH scalar (SENSITIVE - wiped after KEX).
707 protocore_bignum *f; ///< Server DH public value = g^y mod p (sent to client).
708 protocore_bignum *K; ///< Shared DH secret = e^y mod p (SENSITIVE - wiped after key derivation).
709} SshDhState;
710
711/** @brief Pool of ephemeral DH state, one entry per MAX_SSH_CONNS. */
713
714/**
715 * @brief Generate slot @p i's DH ephemeral: a random y, and f = g^y mod p (RFC 4253 sec 8).
716 * @return 0 on success, -1 if the slot has no storage.
717 */
718int ssh_dh_generate(uint8_t i);
719
720/**
721 * @brief Derive the RFC 4253 sec 7.2 keys from K, H and the session id into slot @p i's epoch,
722 * one letter per direction.
723 */
724/**
725 * @brief One RFC 4253 sec 7.2 derivation: @p out_len bytes of the key @p label names.
726 *
727 * "Encryption keys MUST be computed as HASH, of a known value and K": K1 = HASH(K || H || X ||
728 * session_id) with X the label byte, and where more bytes are wanted than one hash gives,
729 * "the key is extended by computing HASH of the concatenation of K and H and the entire key so
730 * far" - K2 = HASH(K || H || K1), K3 = HASH(K || H || K1 || K2), key = K1 || K2 || K3.
731 *
732 * @param label 'A'..'F', the six keys sec 7.2 lists in order.
733 * @param out_len Bytes wanted, clamped to SSH_KDF_MAX.
734 */
735void ssh_kdf_derive(const SshKdfInputs *in, char label, uint8_t *out, size_t out_len);
736
737void ssh_kex_install_keys(uint8_t i, const SshKdfInputs *in);
738
739// ---------------------------------------------------------------------------
740// Wipe helpers
741// ---------------------------------------------------------------------------
742
743/**
744 * @brief Zero all key material for slot @p i on disconnect or KEX failure.
745 *
746 * Each buffer is wiped through its pointer at its own declared length: the bytes are the
747 * connection's, and zeroing the struct would clear the bindings and leave the keys in the span.
748 */
749static inline void ssh_keymat_wipe(uint8_t i)
750{
751 if (i >= MAX_SSH_CONNS)
752 {
753 return;
754 }
755 for (uint8_t e = 0; e < 2u; e++)
756 {
757 SshKeyMat *km = &ssh_keys[i][e];
758 if (km->gcm_ctx_c2s != NULL)
759 {
760 // A keyed GCM context may hold what the accelerator arm attached, so zeroing the bytes alone
761 // would leak it once per closed connection. Release first, then wipe. Each direction owns its
762 // context, and only the direction that negotiated GCM stood one up.
764 {
765 AesGcm.key_wipe(km->gcm_ctx_c2s);
766 }
768 {
769 AesGcm.key_wipe(km->gcm_ctx_s2c);
770 }
771 mmgr_zero_buf(km->gcm_ctx_c2s, PROTOCORE_AESGCM_BORROW);
772 mmgr_zero_buf(km->gcm_ctx_s2c, PROTOCORE_AESGCM_BORROW);
773 mmgr_zero_buf(km->chacha_key_c2s, PROTOCORE_CHACHAPOLY_KEY_LEN);
774 mmgr_zero_buf(km->chacha_key_s2c, PROTOCORE_CHACHAPOLY_KEY_LEN);
775 mmgr_zero_buf(km->mac_key_c2s, 64);
776 mmgr_zero_buf(km->mac_key_s2c, 64);
777 mmgr_zero_buf(km->aes_key_c2s, PROTOCORE_AES256CTR_KEY_LEN);
778 mmgr_zero_buf(km->aes_key_s2c, PROTOCORE_AES256CTR_KEY_LEN);
779 mmgr_zero_buf(km->aes_iv_c2s, PROTOCORE_AES256CTR_CTR_LEN);
780 mmgr_zero_buf(km->aes_iv_s2c, PROTOCORE_AES256CTR_CTR_LEN);
781 km->mac_mode_c2s = 0;
782 km->mac_mode_s2c = 0;
783 km->cipher_mode_c2s = 0;
784 km->cipher_mode_s2c = 0;
785 km->active = PROTO_FALSE;
786 }
787 }
788}
789
790/**
791 * @brief Zero every ephemeral KEX private for slot @p i after keys are derived.
792 *
793 * The scalars live in the connection's kmt region, so the wipe follows the pointers to the bytes.
794 * Zeroing the struct itself would only clear the pointers and leave the scalars in the span. The
795 * X25519 / P-256 pair sits beside y, f and K, so one call covers both of its members.
796 */
797static inline void ssh_dh_wipe(uint8_t i)
798{
799 if (i >= MAX_SSH_CONNS)
800 {
801 return;
802 }
803 if (ssh_dh[i].y != NULL)
804 {
805 mmgr_zero_buf(ssh_dh[i].y, sizeof(protocore_bignum));
806 mmgr_zero_buf(ssh_dh[i].f, sizeof(protocore_bignum));
807 mmgr_zero_buf(ssh_dh[i].K, sizeof(protocore_bignum));
808 }
809 if (ssh_sess[i].ecdh_sk != NULL)
810 {
811 mmgr_zero_buf(ssh_sess[i].ecdh_sk, SSH_ECDH_PAIR_LEN);
812 }
813}
814
815/** @brief Send DISCONNECT with the no-more-auth-methods reason, then drop. */
816/**
817 * @brief Build an SSH_MSG_DISCONNECT payload (RFC 4253 sec 11.1).
818 *
819 * @param reason_code One of SSH_DISCONNECT_* constants.
820 * @param desc Description bytes, sent as the message's first string.
821 * @param desc_len Length of @p desc.
822 * @param out Output buffer for the payload.
823 * @param out_len Set to the number of bytes written.
824 * @param cap Capacity of @p out.
825 * @return 0 on success, -1 when @p cap cannot hold the whole message.
826 */
827int ssh_pkt_build_disconnect(uint32_t reason_code, const char *desc, size_t desc_len, uint8_t *out, size_t *out_len,
828 size_t cap);
829
830/** @brief Dispatch one decrypted message; 50 and above go up to the authentication protocol. */
831int ssh_transport_dispatch(uint8_t i, uint8_t msg_type, const uint8_t *payload, size_t len);
832
833/** @brief Emit a fresh KEXINIT for slot @p i once its volume or time budget is spent. */
835
836/**
837 * @brief Handle SSH_MSG_SERVICE_REQUEST; emit SERVICE_ACCEPT for ssh-userauth (RFC 4253 sec 10).
838 * @return 0 and writes SERVICE_ACCEPT to @p out, or -1 if the service is not "ssh-userauth" or the
839 * message is malformed.
840 */
841int ssh_transport_service_request(const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap);
842
843/**
844 * @brief Take the peer identification string off @p buf (RFC 4253 sec 4.2).
845 * @return 1 once it is whole and the phase has advanced, 0 while more bytes are needed, -1 on a
846 * string the section does not admit. @p off is left at the first binary packet byte.
847 */
848int ssh_transport_version_exchange_recv(uint8_t i, const uint8_t *buf, size_t n, size_t *off);
849
850// ---------------------------------------------------------------------------
851// Public key algorithms (RFC 4253 sec 6.6)
852// ---------------------------------------------------------------------------
853
854// ---------------------------------------------------------------------------
855// RFC 4253 sec 8 - the shared secret K, from the initiating role's side
856// ---------------------------------------------------------------------------
857
858/**
859 * @brief The initiating role's ephemeral, as the sec 8 method needs to read it.
860 *
861 * @var SshKexEphemeral::alg the negotiated method, which selects every term below
862 * @var SshKexEphemeral::priv 32 bytes: X25519 scalar, P-256 d, or the DH exponent
863 * @var SshKexEphemeral::hybrid_sk a PQ/T hybrid's decapsulation key, null for the classical methods
864 * @var SshKexEphemeral::work crypto scratch, borrowed from the caller's slot
865 */
866typedef struct
867{
869 const uint8_t *priv;
870 const uint8_t *hybrid_sk;
871 uint8_t *work;
873
874/**
875 * @brief Compute K from the peer's exchange value, for the role that sent the first message.
876 *
877 * One switch over the negotiated method, beside the responder half that shares this file: RFC 4253
878 * sec 8 is the transport's, whichever end is running it. K is written right-aligned into @p k_be,
879 * which is how sec 8 and sec 7.2 both consume it - as an mpint for the classical methods, and as a
880 * fixed 32 or 64-byte string for the hybrids.
881 *
882 * @param e This end's ephemeral for the exchange.
883 * @param peer_pub The peer's exchange value: Q_S, f, or ciphertext || Q_S for a hybrid.
884 * @param peer_pub_len Length of @p peer_pub; each method checks it against its own.
885 * @param k_be 256 bytes, zeroed then filled from the right.
886 * @return false on a wrong length, a rejected point (RFC 7748 sec 6.1), or an unsupported method.
887 */
888proto_bool ssh_kex_shared_secret(const SshKexEphemeral *e, const uint8_t *peer_pub, uint32_t peer_pub_len,
889 uint8_t k_be[256]);
890
891/** @brief True when @p blob holds a public key in one of the formats this build decodes. */
892proto_bool ssh_pubkey_blob_valid(const uint8_t *blob, uint32_t blob_len);
893
894/**
895 * @brief True when @p pk_algo names an algorithm this end verifies, and @p blob is of its key type.
896 *
897 * RFC 4252 sec 7: "Any public key algorithm may be offered for use in authentication... If the
898 * server does not support some algorithm, it MUST simply reject the request." The name arrives
899 * independently of the blob, so both are checked against what the blob parsers here accept: either
900 * RSA signature name takes an "ssh-rsa" blob, the other two take a blob named for themselves.
901 */
902proto_bool ssh_pubkey_algo_supported(const char *pk_algo, const uint8_t *blob, uint32_t blob_len);
903
904/**
905 * @brief Verify @p sig over @p signed_data against the public key in @p blob, out of slot @p i's
906 * crypto_work. The key type comes from the blob; @p pk_algo steers the RSA signature hash (RFC 8332).
907 */
908proto_bool ssh_pubkey_verify(uint8_t i, const char *pk_algo, const uint8_t *blob, uint32_t blob_len, const uint8_t *sig,
909 uint32_t sig_len, const uint8_t *signed_data, size_t signed_len);
910
912
913#endif // PROTOCORE_TRANSPORT_TRANSPORT_H
AES-256-CTR stream cipher (aes256-ctr, RFC 4344 §4).
AES-256-GCM AEAD (RFC 5116) - keyed, detached tag.
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
chacha20-poly1305@openssh.com AEAD cipher (OpenSSH PROTOCOL.chacha20poly1305).
#define PROTOCORE_AESGCM_BORROW
The handshake phase machine, RFC 4253 sec 4.2 through sec 10.
Root infrastructure: fixed widths, serializers, opcodes and sizes, for every layer above.
#define SSH_ECDH_PAIR_LEN
The ECDH ephemeral pair, private then public: what one wipe covers.
Definition common.h:196
SHA-256 (FIPS 180-4) - streaming and one-shot digest.
#define SSH_KEXHASH_MAX_LEN
Longest exchange hash / session_id the two KEX hashes produce (SHA-512).
Definition ssh_kexhash.h:26
Ephemeral Diffie-Hellman state for one SSH connection.
Definition transport.h:705
protocore_bignum * K
Shared DH secret = e^y mod p (SENSITIVE - wiped after key derivation).
Definition transport.h:708
protocore_bignum * f
Server DH public value = g^y mod p (sent to client).
Definition transport.h:707
protocore_bignum * y
Server ephemeral private DH scalar (SENSITIVE - wiped after KEX).
Definition transport.h:706
One direction's codec state, handed to the packet layer per call.
Definition transport.h:32
uint8_t epoch
key epoch it reads out of ssh_keys[slot][]
Definition transport.h:34
proto_bool enc
that direction's cipher/MAC is active
Definition transport.h:33
One key exchange's derivation inputs, passed by reference.
Definition transport.h:354
const uint8_t * session_id
H of the first KEX; equals H until the first re-key.
Definition transport.h:358
const uint8_t * H
Current exchange hash.
Definition transport.h:357
proto_bool is512
Hash with SHA-512 instead of SHA-256.
Definition transport.h:362
proto_bool k_is_string
Encode K as a plain SSH string (hybrid KEX), not an mpint.
Definition transport.h:361
const uint8_t * K_be
Shared secret K, big-endian, 256 bytes.
Definition transport.h:356
size_t sid_len
Length of session_id.
Definition transport.h:360
uint8_t * work
PROTOCORE_SSH_KDF_BORROW bytes: the hash context, then the K1 || K2 chain.
Definition transport.h:355
size_t h_len
Length of H.
Definition transport.h:359
SshKexAlg alg
Definition transport.h:868
uint8_t * work
Definition transport.h:871
const uint8_t * priv
Definition transport.h:869
const uint8_t * hybrid_sk
Definition transport.h:870
RFC 4253 sec 8: every term the exchange hash H is taken over, and where H lands.
Definition transport.h:195
size_t ks_len
its length
Definition transport.h:204
const uint8_t * spub
the server public value
Definition transport.h:199
size_t cpub_len
its length
Definition transport.h:198
const uint8_t * cpub
the client public value
Definition transport.h:197
const uint8_t * k_be
the shared secret, big-endian
Definition transport.h:201
size_t k_len
its length
Definition transport.h:202
size_t spub_len
its length
Definition transport.h:200
proto_bool k_is_string
that secret is a string, not an mpint
Definition transport.h:205
proto_bool is512
the method hashes with SHA-512
Definition transport.h:206
size_t hash_len
its length: 32 for the SHA-256 methods, 64 for the SHA-512 one
Definition transport.h:208
const uint8_t * ks
the host key blob
Definition transport.h:203
proto_bool pub_is_string
the peer public value is a string, not an mpint
Definition transport.h:196
AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
Definition transport.h:633
uint8_t * chacha_key_s2c
PROTOCORE_CHACHAPOLY_KEY_LEN: server-to-client, used only in chacha mode.
Definition transport.h:657
uint8_t * aes_iv_c2s
PROTOCORE_AES256CTR_CTR_LEN: AES IV C→S (CTR counter / GCM nonce); advances per packet.
Definition transport.h:644
uint8_t mac_mode_s2c
SSH_MAC_* server-to-client (aes256-ctr only).
Definition transport.h:652
proto_bool active
True once keys are installed after successful KEX.
Definition transport.h:671
uint8_t * aes_iv_s2c
PROTOCORE_AES256CTR_CTR_LEN: AES IV S→C (CTR counter / GCM nonce); advances per packet.
Definition transport.h:645
uint8_t * gcm_ctx_s2c
PROTOCORE_AESGCM_BORROW: keyed GCM context S→C (server seals outbound).
Definition transport.h:669
uint8_t * mac_key_c2s
64B: HMAC key, client-to-server (aes mode); 32 bytes for SHA-256, 64 for SHA-512.
Definition transport.h:647
uint8_t * aes_key_s2c
PROTOCORE_AES256CTR_KEY_LEN: AES key S→C (server encrypts outbound).
Definition transport.h:643
uint8_t cipher_mode_s2c
SSH_CIPHER_* server-to-client.
Definition transport.h:654
uint8_t mac_mode_c2s
SSH_MAC_* client-to-server (aes256-ctr only).
Definition transport.h:651
uint8_t * gcm_ctx_c2s
PROTOCORE_AESGCM_BORROW: keyed GCM context C→S (server opens inbound).
Definition transport.h:668
uint8_t * mac_key_s2c
64B: HMAC key, server-to-client (aes mode).
Definition transport.h:648
uint8_t * chacha_key_c2s
PROTOCORE_CHACHAPOLY_KEY_LEN: client-to-server, used only in chacha mode.
Definition transport.h:656
uint8_t * aes_key_c2s
PROTOCORE_AES256CTR_KEY_LEN: AES key C→S (server decrypts inbound).
Definition transport.h:642
uint8_t cipher_mode_c2s
SSH_CIPHER_* client-to-server.
Definition transport.h:653
RFC 4253 sec 6 binary packet: the bytes a receive consumes, and the body it carries.
Definition transport.h:178
const uint8_t * payload
a KEXINIT or KEXDH payload
Definition transport.h:180
const uint8_t * data
bytes a receive consumes
Definition transport.h:179
size_t consumed
bytes a receive took from data
Definition transport.h:182
size_t len
how many
Definition transport.h:181
Per-connection SSH binary packet state.
Definition transport.h:377
proto_bool tx_ready
A packet is framed and waiting for a worker.
Definition transport.h:405
uint8_t * crypto_work
Definition transport.h:415
proto_bool is_client
Definition transport.h:387
uint8_t * cipher_work
Definition transport.h:412
size_t rx_len
Bytes currently in rx_buf.
Definition transport.h:392
uint32_t seq_no_recv
Incoming sequence number.
Definition transport.h:381
size_t tx_len
Bytes of the framed packet.
Definition transport.h:403
uint8_t * mac_work
PROTOCORE_HMAC_SHA256_BORROW bytes. Null until the first packet.
Definition transport.h:409
size_t tx_off
Bytes already put on the wire.
Definition transport.h:404
uint8_t * rx_buf
SSH_RX_ASM_CAP bytes at SSH_OFF_RX_ASM. Null until claimed.
Definition transport.h:391
uint32_t seq_no_send
Outgoing sequence number.
Definition transport.h:380
uint8_t * tx_wire
The wire buffer for this slot. Null until the first packet.
Definition transport.h:402
RFC 4253 sec 9 key re-exchange: what has passed since the last one, against its budget.
Definition transport.h:213
uint32_t pkt_threshold
the volume budget
Definition transport.h:217
uint32_t seq_send
packets sent since the last exchange
Definition transport.h:214
uint32_t seq_recv
packets received since it
Definition transport.h:215
uint32_t time_threshold_ms
the time budget
Definition transport.h:218
uint32_t elapsed_ms
time since it
Definition transport.h:216
SSH transport/session state for one connection (BSS pool).
Definition transport.h:64
uint8_t * ecdh_sk
32B: X25519 scalar / P-256 d, ephemeral private. Wiped by ssh_dh_wipe().
Definition transport.h:78
uint8_t mac_alg_c2s
SSH_MAC_* client-to-server (aes cipher only; 0 = hmac-sha2-256).
Definition transport.h:73
char * v_s
SSH_VERSION_MAX: server identification string (no CR LF).
Definition transport.h:83
uint8_t session_id_len
Session id length (the first KEX's exchange-hash length).
Definition transport.h:98
uint16_t v_s_len
Length of v_s.
Definition transport.h:84
uint8_t * session_id
SSH_KEXHASH_MAX_LEN: H from the first KEX (RFC 4253 sec 7.2).
Definition transport.h:97
proto_bool have_session_id
True once the first KEX completes.
Definition transport.h:99
SshDir in
Our inbound direction: encrypted once the peer's arrives.
Definition transport.h:104
SshKexAlg kex_alg
negotiated in KEXINIT.
Definition transport.h:67
uint8_t * ident_buf
SSH_VERSION_MAX: accumulator for the inbound identification string.
Definition transport.h:86
SshHostkeyAlg hostkey_alg
negotiated in KEXINIT.
Definition transport.h:68
uint8_t * i_c
PROTOCORE_SSH_I_C_MAX: client KEXINIT payload (for H).
Definition transport.h:89
uint16_t i_c_len
Length of i_c.
Definition transport.h:90
SshPhase phase
Current handshake phase.
Definition transport.h:65
SshDir out
Our outbound direction: encrypted once we sent NEWKEYS, and the epoch it reads.
Definition transport.h:103
char * v_c
SSH_VERSION_MAX: client identification string (no CR LF).
Definition transport.h:81
uint16_t i_s_len
Length of i_s.
Definition transport.h:92
proto_bool authed
True after successful user authentication.
Definition transport.h:118
proto_bool kex_active
An exchange is running, from KEXINIT to NEWKEYS (sec 9).
Definition transport.h:106
proto_bool kexinit_sent
This end has sent its KEXINIT and not yet its NEWKEYS (sec 7.1).
Definition transport.h:110
uint8_t * cpub
PROTOCORE_SSH_CPUB_MAX: exchange value the client sent - e, Q_C or C_INIT (for H).
Definition transport.h:94
uint32_t last_kex_ms
protocore_millis() when the last KEX completed.
Definition transport.h:119
proto_bool drop_guessed_kex_pkt
The peer guessed a KEX that lost negotiation (sec 7.1).
Definition transport.h:115
uint16_t cpub_len
Length of cpub.
Definition transport.h:95
uint8_t * ecdh_pk
32B: X25519 ephemeral public (curve25519 KEX only).
Definition transport.h:79
proto_bool ext_info_enabled
Peer offered its role's RFC 8308 sec 2.2 indicator.
Definition transport.h:116
proto_bool ext_info_sent
EXT_INFO already went out; RFC 8308 sec 2.4 allows it once.
Definition transport.h:117
uint8_t * i_s
PROTOCORE_SSH_I_S_MAX: server KEXINIT payload (for H).
Definition transport.h:91
uint8_t cipher_alg_s2c
SSH_CIPHER_* server-to-client.
Definition transport.h:72
SshPhase phase_before_kex
What to resume when this exchange completes (sec 9).
Definition transport.h:114
uint8_t cipher_alg_c2s
SSH_CIPHER_* client-to-server.
Definition transport.h:71
uint16_t ident_len
Bytes buffered in ident_buf.
Definition transport.h:87
uint8_t mac_alg_s2c
SSH_MAC_* server-to-client (aes cipher only; 0 = hmac-sha2-256).
Definition transport.h:74
uint16_t v_c_len
Length of v_c.
Definition transport.h:82
The entries.
Definition transport.h:267
void(*const recv_ident)(uint8_t *work)
Definition transport.h:268
Where a build or a send writes, and what it wrote.
Definition transport.h:187
size_t out_len
what it wrote
Definition transport.h:189
uint8_t * out
where a build or a send writes
Definition transport.h:188
size_t cap
how much room it has
Definition transport.h:190
SshRekeyArgs rekey
sec 9 the volume and time budget since the last exchange
Definition transport.h:257
SshPacketArgs pkt
sec 6 the bytes one message occupies
Definition transport.h:254
proto_bool ok
Definition transport.h:258
SshTransportOut out_args
where a build or a send writes
Definition transport.h:255
SshKexHashArgs kexhash
sec 8 the terms the exchange hash H is taken over
Definition transport.h:256
uint8_t slot
the SSH slot a call acts on
Definition transport.h:253
uint8_t * protocore_ssh_transport_span(void)
The PROTOCORE_SSH_TRANSPORT_BORROW bytes this module's state lives in.
void protocore_ssh_transport_begin_rekey(uint8_t *work)
SshPacketState ssh_pkt[MAX_SSH_CONNS]
Static packet state pool (BSS). One entry per SSH slot.
SshTransportVars SshTransportV
The operands and the outcome.
int ssh_pkt_emit(uint8_t i, const uint8_t *payload, size_t len, const SshDir *dir)
Frame payload for slot i into the secure pool and raise the flag a worker drains.
proto_bool ssh_pubkey_verify(uint8_t i, const char *pk_algo, const uint8_t *blob, uint32_t blob_len, const uint8_t *sig, uint32_t sig_len, const uint8_t *signed_data, size_t signed_len)
Verify sig over signed_data against the public key in blob, out of slot i's crypto_work....
void protocore_ssh_transport_recv_ident(uint8_t *work)
enum PROTO_ENUM_PACKED SshKexAlg
Negotiated key-exchange method.
const uint8_t * ssh_session_id(uint8_t i, size_t *len)
The session identifier for slot i, or null before the first key exchange completes.
int ssh_pkt_build_disconnect(uint32_t reason_code, const char *desc, size_t desc_len, uint8_t *out, size_t *out_len, size_t cap)
Send DISCONNECT with the no-more-auth-methods reason, then drop.
void protocore_ssh_transport_rekey_due(uint8_t *work)
proto_bool ssh_pubkey_blob_valid(const uint8_t *blob, uint32_t blob_len)
True when blob holds a public key in one of the formats this build decodes.
proto_bool ssh_pubkey_algo_supported(const char *pk_algo, const uint8_t *blob, uint32_t blob_len)
True when pk_algo names an algorithm this end verifies, and blob is of its key type.
enum PROTO_ENUM_PACKED SshHostkeyAlg
Negotiated host-key / signature algorithm.
void protocore_ssh_transport_newkeys_sent(uint8_t *work)
SshKeyMat ssh_keys[MAX_SSH_CONNS][2]
Pool of session key material, two epochs per MAX_SSH_CONNS.
void protocore_ssh_transport_send_ident(uint8_t *work)
void protocore_ssh_transport_kexinit_build(uint8_t *work)
void protocore_ssh_transport_newkeys_complete(uint8_t *work)
int ssh_pkt_send(uint8_t i, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len, size_t out_cap, const SshDir *dir)
Build and send one SSH binary packet.
proto_bool ssh_hostkey_verify(uint8_t i, const uint8_t *ks, size_t ks_len, const uint8_t *sig, size_t sig_len, const uint8_t *h, size_t h_len)
Verify the server's signature over the exchange hash with its host key (RFC 4253 sec 8).
void protocore_ssh_transport_kex_generate(uint8_t *work)
void protocore_ssh_transport_exchange_hash(uint8_t *work)
int ssh_pkt_send_at(uint8_t i, uint8_t *wire, size_t payload_len, size_t *out_len, size_t wire_cap, const SshDir *dir)
Frame the payload_len bytes already written at wire + SSH_WIRE_PAYLOAD_OFF.
int ssh_transport_dispatch(uint8_t i, uint8_t msg_type, const uint8_t *payload, size_t len)
Dispatch one decrypted message; 50 and above go up to the authentication protocol.
proto_bool ssh_kex_prefer_rsa(void)
Current negotiation preference (true = prefer RSA / DH).
void ssh_transport_key_re_exchange(uint8_t i)
Emit a fresh KEXINIT for slot i once its volume or time budget is spent.
int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler, const SshDir *dir)
Receive and process one or more SSH binary packets from data.
int ssh_pkt_unimplemented(uint8_t i, uint8_t *out, size_t *out_len, size_t out_cap)
Build the SSH_MSG_UNIMPLEMENTED payload answering the packet slot i last received.
void ssh_kex_set_prefer_rsa(proto_bool prefer)
Steer KEX and host-key negotiation toward RSA with DH-group14, or toward curve25519 with ed25519.
void ssh_session_id_latch(uint8_t i, const uint8_t *h, size_t h_len)
Latch the first exchange's hash as slot i's session identifier (RFC 4253 sec 7.2).
@ SSH_CIPHER_CHACHA20POLY1305
chacha20-poly1305@openssh.com (AEAD; no separate MAC)
Definition transport.h:581
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
Definition transport.h:582
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
Definition transport.h:580
int ssh_transport_version_exchange_recv(uint8_t i, const uint8_t *buf, size_t n, size_t *off)
Take the peer identification string off buf (RFC 4253 sec 4.2).
int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap, const SshDir *dir)
Send SSH_MSG_DISCONNECT with reason reason_code.
SshSession ssh_sess[MAX_SSH_CONNS]
Static pool of SSH session state (BSS), one per SSH slot.
@ SSH_HOSTKEY_RSA_SHA512
rsa-sha2-512 (RFC 8332)
Definition transport.h:52
@ SSH_KEX_DH_GROUP14
diffie-hellman-group14-sha256 (RFC 8268)
Definition transport.h:40
@ SSH_KEX_SNTRUP761_X25519
sntrup761x25519-sha512@openssh.com
Definition transport.h:44
@ SSH_KEX_CURVE25519
curve25519-sha256 (RFC 8731)
Definition transport.h:41
@ SSH_HOSTKEY_ED25519
ssh-ed25519 (RFC 8709)
Definition transport.h:51
@ SSH_KEX_MLKEM768_X25519
mlkem768x25519-sha256
Definition transport.h:42
@ SSH_HOSTKEY_RSA_SHA256
rsa-sha2-256 (RFC 8332)
Definition transport.h:50
@ SSH_HOSTKEY_ECDSA_NISTP256
ecdsa-sha2-nistp256 (RFC 5656)
Definition transport.h:53
@ SSH_KEX_ECDH_NISTP256
ecdh-sha2-nistp256 (RFC 5656 sec 4)
Definition transport.h:43
SshDhState ssh_dh[MAX_SSH_CONNS]
Pool of ephemeral DH state, one entry per MAX_SSH_CONNS.
@ SSH_MAC_HMAC_SHA256_ETM
hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
Definition transport.h:590
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
Definition transport.h:588
@ SSH_MAC_HMAC_SHA512
hmac-sha2-512 (encrypt-and-MAC)
Definition transport.h:589
@ SSH_MAC_HMAC_SHA512_ETM
hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
Definition transport.h:591
void protocore_ssh_transport_kexinit_parse(uint8_t *work)
void ssh_transport_init(uint8_t i)
Bind the session state for SSH connection slot i to the slot's storage.
void ssh_pkt_init(uint8_t i)
Initialize the packet state for SSH connection slot i.
proto_bool ssh_pkt_slot_storage(SshPacketState *s)
Take the slot's one persistent borrow if it has none yet, and split it.
proto_bool ssh_kex_is_sha512(SshKexAlg a)
True when a hashes with SHA-512 rather than SHA-256 (RFC 8268, RFC 8731).
void ssh_pkt_set_client(uint8_t i)
Mark slot i as the SSH client role (call once, right after ssh_pkt_init).
void ssh_kdf_derive(const SshKdfInputs *in, char label, uint8_t *out, size_t out_len)
Derive the RFC 4253 sec 7.2 keys from K, H and the session id into slot i's epoch,...
int ssh_transport_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 (RFC 4253 sec 10).
proto_bool ssh_kex_shared_secret(const SshKexEphemeral *e, const uint8_t *peer_pub, uint32_t peer_pub_len, uint8_t k_be[256])
Compute K from the peer's exchange value, for the role that sent the first message.
void(* ssh_msg_handler_t)(uint8_t slot, uint8_t msg_type, const uint8_t *payload, size_t payload_len)
Callback invoked once per complete, verified inbound SSH message.
Definition transport.h:511
void ssh_kex_install_keys(uint8_t i, const SshKdfInputs *in)
void protocore_ssh_transport_kexdh_reply(uint8_t *work)
int ssh_dh_generate(uint8_t i)
Generate slot i's DH ephemeral: a random y, and f = g^y mod p (RFC 4253 sec 8).
#define PROTO_FALSE
the false value
Definition types.h:68
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97