ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_packet.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_packet.h
6 * @brief SSH binary packet protocol: framing, AES-256-CTR encryption,
7 * HMAC-SHA2-256 integrity, and sequence-number tracking.
8 *
9 * ═══════════════════════════════════════════════════════════════════════════
10 * WIRE FORMAT (RFC 4253 §6)
11 * ═══════════════════════════════════════════════════════════════════════════
12 *
13 * Each SSH packet on the wire (after KEX completes):
14 *
15 * [4 bytes] packet_length - big-endian uint32 (does NOT include itself
16 * or the MAC; includes padding_length, payload,
17 * and random_padding fields)
18 * [1 byte] padding_length - number of bytes of random padding
19 * [N bytes] payload - the SSH message
20 * [P bytes] random_padding - random bytes; total pkt_len % blocksize == 0
21 * [32 bytes] MAC - HMAC-SHA2-256 over:
22 * uint32(seq_no) || packet_length ||
23 * padding_length || payload || random_padding
24 *
25 * AES-256-CTR encrypts: packet_length || padding_length || payload || padding
26 * The MAC is computed over the PLAINTEXT (before encryption) prepended with
27 * the 4-byte sequence number. This is the "encrypt-then-MAC" variant used
28 * by openssh with hmac-sha2-256 (ETM) - but RFC 4253 default is MAC-then-
29 * encrypt. We implement STANDARD RFC 4253 (MAC over plaintext, then encrypt)
30 * to match the base SSH specification.
31 *
32 * ═══════════════════════════════════════════════════════════════════════════
33 * MAC-VERIFY-BEFORE-USE INVARIANT
34 * ═══════════════════════════════════════════════════════════════════════════
35 *
36 * On receive: decrypt → verify HMAC → then process the payload.
37 * If HMAC verification fails: close the connection immediately, zero the
38 * decrypted buffer. Do NOT process or reflect any byte of the payload.
39 *
40 * This ordering closes the "BEAST" class of attack where an attacker can
41 * influence decryption outputs by injecting invalid packets.
42 *
43 * ═══════════════════════════════════════════════════════════════════════════
44 * SEQUENCE NUMBER OVERFLOW GUARD
45 * ═══════════════════════════════════════════════════════════════════════════
46 *
47 * SSH sequence numbers are 32-bit values that wrap at 2^32. RFC 4253 does
48 * not mandate rekeying before wrap; however, CTR-mode keystream repetition
49 * at wrap would be a catastrophic confidentiality failure.
50 *
51 * Policy: close the connection if seq_no_send or seq_no_recv reaches
52 * SSH_SEQ_CLOSE_THRESHOLD. A future rekey implementation would reset the
53 * counters instead.
54 *
55 * ═══════════════════════════════════════════════════════════════════════════
56 * PADDING
57 * ═══════════════════════════════════════════════════════════════════════════
58 *
59 * AES-256-CTR block size = 16 bytes. RFC 4253 §6 requires the padded
60 * packet (packet_length + 1 + payload + padding) to be a multiple of
61 * max(8, cipher_block_size) = 16. Minimum padding is 4 bytes.
62 *
63 * padding_len = (16 - ((5 + payload_len) % 16)) % 16
64 * if (padding_len < 4) padding_len += 16;
65 *
66 * @author Douglas Quigg (dstroy0)
67 * @date 2026
68 */
69
70#ifndef PROTOCORE_SSH_PACKET_H
71#define PROTOCORE_SSH_PACKET_H
72
75#include "protocore_config.h"
76#include <stddef.h>
77#include <stdint.h>
78
79// ---------------------------------------------------------------------------
80// Sequence number overflow threshold
81// ---------------------------------------------------------------------------
82
83/**
84 * @brief Close the connection when seq_no reaches this value.
85 *
86 * Set to 0xFFFFFFF0 (16 below the 32-bit wrap) as a conservative margin.
87 * This prevents CTR keystream reuse that would occur at wrap.
88 * A rekey implementation would reset this counter; until then, we close.
89 */
90#define SSH_SEQ_CLOSE_THRESHOLD 0xFFFFFFF0u
91
92// ---------------------------------------------------------------------------
93// SSH message type constants (RFC 4253)
94// ---------------------------------------------------------------------------
95
96#define SSH_MSG_DISCONNECT 1
97#define SSH_MSG_IGNORE 2
98#define SSH_MSG_UNIMPLEMENTED 3
99#define SSH_MSG_SERVICE_REQUEST 5
100#define SSH_MSG_SERVICE_ACCEPT 6
101#define SSH_MSG_EXT_INFO 7 // RFC 8308 extension negotiation
102#define SSH_MSG_KEXINIT 20
103#define SSH_MSG_NEWKEYS 21
104#define SSH_MSG_KEXDH_INIT 30
105#define SSH_MSG_KEXDH_REPLY 31
106#define SSH_MSG_USERAUTH_REQUEST 50
107#define SSH_MSG_USERAUTH_FAILURE 51
108#define SSH_MSG_USERAUTH_SUCCESS 52
109#define SSH_MSG_USERAUTH_PK_OK 60
110// 60 is method-specific: it is PK_OK for publickey and INFO_REQUEST for keyboard-interactive
111// (RFC 4256 §3.2). The current auth phase/state disambiguates which handler owns an inbound 60.
112#define SSH_MSG_USERAUTH_INFO_REQUEST 60 // RFC 4256 §3.2 (keyboard-interactive, server->client)
113#define SSH_MSG_USERAUTH_INFO_RESPONSE 61 // RFC 4256 §3.4 (keyboard-interactive, client->server)
114#define SSH_MSG_GLOBAL_REQUEST 80 // RFC 4254 §4 (e.g. tcpip-forward for ssh -R)
115#define SSH_MSG_REQUEST_SUCCESS 81 // RFC 4254 §4 reply to a want_reply global request
116#define SSH_MSG_REQUEST_FAILURE 82 // RFC 4254 §4 reply: request refused / unrecognized
117#define SSH_MSG_CHANNEL_OPEN 90
118#define SSH_MSG_CHANNEL_OPEN_CONFIRM 91
119#define SSH_MSG_CHANNEL_OPEN_FAILURE 92
120#define SSH_MSG_CHANNEL_WINDOW_ADJUST 93
121#define SSH_MSG_CHANNEL_DATA 94
122#define SSH_MSG_CHANNEL_EOF 96
123#define SSH_MSG_CHANNEL_CLOSE 97
124#define SSH_MSG_CHANNEL_REQUEST 98
125#define SSH_MSG_CHANNEL_SUCCESS 99
126#define SSH_MSG_CHANNEL_FAILURE 100
127
128// ---------------------------------------------------------------------------
129// Disconnect reason codes (RFC 4253 §11.1)
130// ---------------------------------------------------------------------------
131
132#define SSH_DISCONNECT_PROTOCOL_ERROR 2
133#define SSH_DISCONNECT_MAC_ERROR 5
134#define SSH_DISCONNECT_TOO_MANY_CONNECTIONS 11
135#define SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE 14 // RFC 4250 §4.2.2
136
137// ---------------------------------------------------------------------------
138// Packet state per connection
139// ---------------------------------------------------------------------------
140
141/**
142 * @brief Per-connection SSH binary packet state.
143 *
144 * Allocated in ssh_pool[] (BSS); one entry per SSH connection slot.
145 * Key material is in ssh_keys[] - a separate BSS symbol to prevent linear
146 * overflow from packet buffers into key material.
147 */
149{
150 uint32_t seq_no_send; ///< Outgoing sequence number (incremented per packet).
151 uint32_t seq_no_recv; ///< Incoming sequence number (incremented per packet).
152 bool kex_active; ///< True while KEX is in progress (no user data).
153 // Encryption activates per direction (RFC 4253 sec 7.3): our outbound turns on when we send our
154 // SSH_MSG_NEWKEYS, our inbound when we receive the peer's. The send path (pack) reads enc_out; the
155 // receive path (unpack) reads enc_in. A strict peer may activate its send direction before we
156 // activate ours, so the two are tracked independently rather than as one flag.
157 bool enc_out; ///< True once we have sent our NEWKEYS (outbound cipher/MAC active).
158 bool enc_in; ///< True once we have received the peer's NEWKEYS (inbound cipher/MAC active).
159
160 // SSH keys are named by direction (client->server "c2s", server->client "s2c"), fixed by RFC 4253
161 // §7.2 regardless of role. A server sends s2c / receives c2s; a client is the mirror. This flag
162 // selects the direction at each cipher/MAC site so one packet implementation serves both roles.
163 // Default false = server (so existing server code is unchanged); ssh_pkt_set_client() flips it.
165
166 // Receive reassembly: we may receive partial packets across TCP segments.
167 uint8_t rx_buf[SSH_PKT_BUF_SIZE]; ///< Raw receive buffer (from transport).
168 size_t rx_len; ///< Bytes currently in rx_buf.
169};
170
171/** @brief Static packet state pool (BSS). One entry per SSH slot. */
173
174// ---------------------------------------------------------------------------
175// Wire buffer sizing
176// ---------------------------------------------------------------------------
177
178// Worst-case on-wire bytes for a payload of up to SSH_PKT_BUF_SIZE: the 4-byte packet_length, the
179// 1-byte padding_length, the effective payload, worst-case padding, and the largest MAC tag. When
180// s2c compression is built in, the "effective payload" is the compressor's worst-case output
181// (ssh_deflate_bound of a full payload) since fixed-Huffman can slightly expand incompressible data.
182// Callers MUST size the wire buffer with this so a compressed packet never overflows and desyncs the
183// stateful cipher / compression stream (a dropped packet mid-stream would corrupt the session).
184#if PC_ENABLE_SSH_ZLIB
185#define SSH_MAX_EFFECTIVE_PAYLOAD (2 + SSH_PKT_BUF_SIZE + (SSH_PKT_BUF_SIZE >> 3) + 32) // = ssh_deflate_bound()
186#else
187#define SSH_MAX_EFFECTIVE_PAYLOAD (SSH_PKT_BUF_SIZE)
188#endif
189#define SSH_MAX_PAD 32 // worst-case padding across block-8 / block-16 modes (min-4 rule)
190#define SSH_MAX_MAC 64 // largest MAC tag (hmac-sha2-512); chacha's Poly1305 tag is 16
191#define SSH_WIRE_CAP ((size_t)(4 + 1 + SSH_MAX_EFFECTIVE_PAYLOAD + SSH_MAX_PAD + SSH_MAX_MAC))
192
193// ---------------------------------------------------------------------------
194// Public API
195// ---------------------------------------------------------------------------
196
197/**
198 * @brief Initialize the packet state for SSH connection slot @p i.
199 *
200 * Zeroes seq numbers; sets encrypted=false, kex_active=true.
201 *
202 * @param i SSH slot index.
203 */
204void ssh_pkt_init(uint8_t i);
205
206/**
207 * @brief Mark slot @p i as the SSH client role (call once, right after ssh_pkt_init).
208 *
209 * Flips the send/receive key direction: the client encrypts with the c2s key set and decrypts with
210 * the s2c one, the mirror of the server. Without this a slot defaults to the server role.
211 */
212void ssh_pkt_set_client(uint8_t i);
213
214/**
215 * @brief Build and send one SSH binary packet.
216 *
217 * Frames @p payload according to RFC 4253 §6:
218 * - Adds random padding to align to 16-byte boundary.
219 * - If encrypted: encrypts with AES-256-CTR, appends HMAC-SHA2-256 MAC.
220 * - Increments seq_no_send; closes connection if threshold reached.
221 *
222 * The serialized packet is written into @p out. *@p out_len is set to the
223 * number of bytes written. @p out must be at least
224 * (4 + 1 + payload_len + 16 + 32) bytes.
225 *
226 * @param i SSH slot index.
227 * @param payload Plaintext SSH message payload.
228 * @param payload_len Length of @p payload.
229 * @param out Output buffer for the wire packet.
230 * @param out_len Set to the number of bytes written into @p out.
231 * @param out_cap Capacity of @p out.
232 * @return 0 on success, -1 on overflow or sequence-number exhaustion.
233 */
234int 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);
235
236/**
237 * @brief Callback invoked once per complete, verified inbound SSH message.
238 *
239 * @param slot SSH slot index.
240 * @param msg_type First payload byte (SSH message number).
241 * @param payload Decrypted message payload (includes @p msg_type at [0]).
242 * @param payload_len Length of @p payload.
243 */
244typedef void (*ssh_msg_handler_t)(uint8_t slot, uint8_t msg_type, const uint8_t *payload, size_t payload_len);
245
246/**
247 * @brief Receive and process one or more SSH binary packets from @p data.
248 *
249 * Appends @p len bytes from @p data to the receive buffer for slot @p i,
250 * then extracts complete packets. For each complete packet:
251 * - If encrypted: decrypts with AES-256-CTR, verifies HMAC-SHA2-256.
252 * Closes connection (returns -1) on MAC failure without processing payload.
253 * - Increments seq_no_recv; closes connection if threshold reached.
254 * - Calls @p handler(slot, msg_type, payload, payload_len) for the payload.
255 *
256 * @param i SSH slot index.
257 * @param data Received bytes (from TCP).
258 * @param len Number of bytes in @p data.
259 * @param handler Callback invoked once per complete, verified packet.
260 * @return 0 on success, -1 on MAC failure or sequence-number exhaustion
261 * (caller must close the TCP connection).
262 */
263int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler);
264
265/**
266 * @brief Send SSH_MSG_DISCONNECT with reason @p reason_code.
267 *
268 * Sends the packet, then zeroes the packet state and key material for slot @p i.
269 *
270 * @param i SSH slot index.
271 * @param reason_code One of SSH_DISCONNECT_* constants.
272 * @param out Output buffer for the wire packet.
273 * @param out_len Set to the number of bytes written.
274 * @param out_cap Capacity of @p out.
275 * @return 0 on success, -1 on error.
276 */
277int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap);
278
279#endif // PROTOCORE_SSH_PACKET_H
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
User-facing configuration for ProtoCore.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
SSH session key material - types, pools, and security model.
int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap)
Send SSH_MSG_DISCONNECT with reason reason_code.
SshPacketState ssh_pkt[MAX_SSH_CONNS]
Static packet state pool (BSS). One entry per SSH slot.
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)
Build and send one SSH binary packet.
void ssh_pkt_init(uint8_t i)
Initialize the packet state for SSH connection slot i.
void ssh_pkt_set_client(uint8_t i)
Mark slot i as the SSH client role (call once, right after ssh_pkt_init).
int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler)
Receive and process one or more SSH binary packets from data.
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 ssh_packet.h:244
Per-connection SSH binary packet state.
Definition ssh_packet.h:149
size_t rx_len
Bytes currently in rx_buf.
Definition ssh_packet.h:168
bool enc_out
True once we have sent our NEWKEYS (outbound cipher/MAC active).
Definition ssh_packet.h:157
uint32_t seq_no_recv
Incoming sequence number (incremented per packet).
Definition ssh_packet.h:151
uint8_t rx_buf[SSH_PKT_BUF_SIZE]
Raw receive buffer (from transport).
Definition ssh_packet.h:167
bool enc_in
True once we have received the peer's NEWKEYS (inbound cipher/MAC active).
Definition ssh_packet.h:158
bool kex_active
True while KEX is in progress (no user data).
Definition ssh_packet.h:152
uint32_t seq_no_send
Outgoing sequence number (incremented per packet).
Definition ssh_packet.h:150