DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETERMINISTICESPASYNCWEBSERVER_SSH_PACKET_H
71#define DETERMINISTICESPASYNCWEBSERVER_SSH_PACKET_H
72
73#include "ServerConfig.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#define SSH_MSG_GLOBAL_REQUEST 80 // RFC 4254 §4 (e.g. tcpip-forward for ssh -R)
111#define SSH_MSG_REQUEST_SUCCESS 81 // RFC 4254 §4 reply to a want_reply global request
112#define SSH_MSG_REQUEST_FAILURE 82 // RFC 4254 §4 reply: request refused / unrecognized
113#define SSH_MSG_CHANNEL_OPEN 90
114#define SSH_MSG_CHANNEL_OPEN_CONFIRM 91
115#define SSH_MSG_CHANNEL_OPEN_FAILURE 92
116#define SSH_MSG_CHANNEL_WINDOW_ADJUST 93
117#define SSH_MSG_CHANNEL_DATA 94
118#define SSH_MSG_CHANNEL_EOF 96
119#define SSH_MSG_CHANNEL_CLOSE 97
120#define SSH_MSG_CHANNEL_REQUEST 98
121#define SSH_MSG_CHANNEL_SUCCESS 99
122#define SSH_MSG_CHANNEL_FAILURE 100
123
124// ---------------------------------------------------------------------------
125// Disconnect reason codes (RFC 4253 §11.1)
126// ---------------------------------------------------------------------------
127
128#define SSH_DISCONNECT_PROTOCOL_ERROR 2
129#define SSH_DISCONNECT_MAC_ERROR 5
130#define SSH_DISCONNECT_TOO_MANY_CONNECTIONS 11
131#define SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE 14 // RFC 4250 §4.2.2
132
133// ---------------------------------------------------------------------------
134// Packet state per connection
135// ---------------------------------------------------------------------------
136
137/**
138 * @brief Per-connection SSH binary packet state.
139 *
140 * Allocated in ssh_pool[] (BSS); one entry per SSH connection slot.
141 * Key material is in ssh_keys[] - a separate BSS symbol to prevent linear
142 * overflow from packet buffers into key material.
143 */
145{
146 uint32_t seq_no_send; ///< Outgoing sequence number (incremented per packet).
147 uint32_t seq_no_recv; ///< Incoming sequence number (incremented per packet).
148 bool kex_active; ///< True while KEX is in progress (no user data).
149 // Encryption activates per direction (RFC 4253 sec 7.3): our outbound turns on when we send our
150 // SSH_MSG_NEWKEYS, our inbound when we receive the peer's. The send path (pack) reads enc_out; the
151 // receive path (unpack) reads enc_in. A strict peer may activate its send direction before we
152 // activate ours, so the two are tracked independently rather than as one flag.
153 bool enc_out; ///< True once we have sent our NEWKEYS (outbound cipher/MAC active).
154 bool enc_in; ///< True once we have received the peer's NEWKEYS (inbound cipher/MAC active).
155
156 // Receive reassembly: we may receive partial packets across TCP segments.
157 uint8_t rx_buf[SSH_PKT_BUF_SIZE]; ///< Raw receive buffer (from transport).
158 size_t rx_len; ///< Bytes currently in rx_buf.
159};
160
161/** @brief Static packet state pool (BSS). One entry per SSH slot. */
163
164// ---------------------------------------------------------------------------
165// Wire buffer sizing
166// ---------------------------------------------------------------------------
167
168// Worst-case on-wire bytes for a payload of up to SSH_PKT_BUF_SIZE: the 4-byte packet_length, the
169// 1-byte padding_length, the effective payload, worst-case padding, and the largest MAC tag. When
170// s2c compression is built in, the "effective payload" is the compressor's worst-case output
171// (ssh_deflate_bound of a full payload) since fixed-Huffman can slightly expand incompressible data.
172// Callers MUST size the wire buffer with this so a compressed packet never overflows and desyncs the
173// stateful cipher / compression stream (a dropped packet mid-stream would corrupt the session).
174#if DETWS_ENABLE_SSH_ZLIB
175#define SSH_MAX_EFFECTIVE_PAYLOAD (2 + SSH_PKT_BUF_SIZE + (SSH_PKT_BUF_SIZE >> 3) + 32) // = ssh_deflate_bound()
176#else
177#define SSH_MAX_EFFECTIVE_PAYLOAD (SSH_PKT_BUF_SIZE)
178#endif
179#define SSH_MAX_PAD 32 // worst-case padding across block-8 / block-16 modes (min-4 rule)
180#define SSH_MAX_MAC 64 // largest MAC tag (hmac-sha2-512); chacha's Poly1305 tag is 16
181#define SSH_WIRE_CAP ((size_t)(4 + 1 + SSH_MAX_EFFECTIVE_PAYLOAD + SSH_MAX_PAD + SSH_MAX_MAC))
182
183// ---------------------------------------------------------------------------
184// Public API
185// ---------------------------------------------------------------------------
186
187/**
188 * @brief Initialize the packet state for SSH connection slot @p i.
189 *
190 * Zeroes seq numbers; sets encrypted=false, kex_active=true.
191 *
192 * @param i SSH slot index.
193 */
194void ssh_pkt_init(uint8_t i);
195
196/**
197 * @brief Build and send one SSH binary packet.
198 *
199 * Frames @p payload according to RFC 4253 §6:
200 * - Adds random padding to align to 16-byte boundary.
201 * - If encrypted: encrypts with AES-256-CTR, appends HMAC-SHA2-256 MAC.
202 * - Increments seq_no_send; closes connection if threshold reached.
203 *
204 * The serialized packet is written into @p out. *@p out_len is set to the
205 * number of bytes written. @p out must be at least
206 * (4 + 1 + payload_len + 16 + 32) bytes.
207 *
208 * @param i SSH slot index.
209 * @param payload Plaintext SSH message payload.
210 * @param payload_len Length of @p payload.
211 * @param out Output buffer for the wire packet.
212 * @param out_len Set to the number of bytes written into @p out.
213 * @param out_cap Capacity of @p out.
214 * @return 0 on success, -1 on overflow or sequence-number exhaustion.
215 */
216int 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);
217
218/**
219 * @brief Callback invoked once per complete, verified inbound SSH message.
220 *
221 * @param slot SSH slot index.
222 * @param msg_type First payload byte (SSH message number).
223 * @param payload Decrypted message payload (includes @p msg_type at [0]).
224 * @param payload_len Length of @p payload.
225 */
226typedef void (*ssh_msg_handler_t)(uint8_t slot, uint8_t msg_type, const uint8_t *payload, size_t payload_len);
227
228/**
229 * @brief Receive and process one or more SSH binary packets from @p data.
230 *
231 * Appends @p len bytes from @p data to the receive buffer for slot @p i,
232 * then extracts complete packets. For each complete packet:
233 * - If encrypted: decrypts with AES-256-CTR, verifies HMAC-SHA2-256.
234 * Closes connection (returns -1) on MAC failure without processing payload.
235 * - Increments seq_no_recv; closes connection if threshold reached.
236 * - Calls @p handler(slot, msg_type, payload, payload_len) for the payload.
237 *
238 * @param i SSH slot index.
239 * @param data Received bytes (from TCP).
240 * @param len Number of bytes in @p data.
241 * @param handler Callback invoked once per complete, verified packet.
242 * @return 0 on success, -1 on MAC failure or sequence-number exhaustion
243 * (caller must close the TCP connection).
244 */
245int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler);
246
247/**
248 * @brief Send SSH_MSG_DISCONNECT with reason @p reason_code.
249 *
250 * Sends the packet, then zeroes the packet state and key material for slot @p i.
251 *
252 * @param i SSH slot index.
253 * @param reason_code One of SSH_DISCONNECT_* constants.
254 * @param out Output buffer for the wire packet.
255 * @param out_len Set to the number of bytes written.
256 * @param out_cap Capacity of @p out.
257 * @return 0 on success, -1 on error.
258 */
259int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap);
260
261#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_PACKET_H
User-facing configuration for DeterministicESPAsyncWebServer.
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
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.
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:226
Per-connection SSH binary packet state.
Definition ssh_packet.h:145
size_t rx_len
Bytes currently in rx_buf.
Definition ssh_packet.h:158
bool enc_out
True once we have sent our NEWKEYS (outbound cipher/MAC active).
Definition ssh_packet.h:153
uint32_t seq_no_recv
Incoming sequence number (incremented per packet).
Definition ssh_packet.h:147
uint8_t rx_buf[SSH_PKT_BUF_SIZE]
Raw receive buffer (from transport).
Definition ssh_packet.h:157
bool enc_in
True once we have received the peer's NEWKEYS (inbound cipher/MAC active).
Definition ssh_packet.h:154
bool kex_active
True while KEX is in progress (no user data).
Definition ssh_packet.h:148
uint32_t seq_no_send
Outgoing sequence number (incremented per packet).
Definition ssh_packet.h:146