DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_packet.cpp
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.cpp
6 * @brief SSH binary packet framing, encryption, MAC, and receive reassembly.
7 */
8
15#if DETWS_ENABLE_SSH_ZLIB
18#endif
20#include <string.h>
21
22#ifdef ARDUINO
23#include <Arduino.h> // esp_fill_random()
24#else
25#include <Arduino.h> // mock
26#endif
27
28// ---------------------------------------------------------------------------
29// BSS allocation
30// ---------------------------------------------------------------------------
31
33
34// ---------------------------------------------------------------------------
35// Internal helpers
36// ---------------------------------------------------------------------------
37
38static inline uint32_t read_u32_be(const uint8_t *p)
39{
40 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
41}
42
43static inline void write_u32_be(uint8_t *p, uint32_t v)
44{
45 p[0] = (uint8_t)(v >> 24);
46 p[1] = (uint8_t)(v >> 16);
47 p[2] = (uint8_t)(v >> 8);
48 p[3] = (uint8_t)(v);
49}
50
51// Compute the padding needed so that (5 + payload_len + padding) is a
52// multiple of 16 (AES block size). Minimum padding = 4 bytes (RFC 4253 §6).
53static size_t compute_padding(size_t payload_len)
54{
55 size_t total = 5 + payload_len; // 4-byte length + 1-byte padding_len + payload
56 size_t remainder = total % 16;
57 size_t padding = (remainder == 0) ? 0 : (16 - remainder);
58 if (padding < 4)
59 padding += 16;
60 return padding;
61}
62
63// Compute the MAC over 4-byte seq_no || buf using the HMAC named by mac_mode. For E&M the buf is
64// the plaintext packet; for ETM it is the length field || ciphertext. Writes ssh_mac_len() bytes.
65static void compute_mac_mode(uint8_t mac_mode, const uint8_t *mac_key, uint32_t seq_no, const uint8_t *buf,
66 size_t buf_len, uint8_t *mac_out)
67{
68 uint8_t seq_be[4];
69 write_u32_be(seq_be, seq_no);
70 if (mac_mode == SSH_MAC_HMAC_SHA512 || mac_mode == SSH_MAC_HMAC_SHA512_ETM)
71 {
73 ssh_hmac_sha512_init(&ctx, mac_key, 64);
74 ssh_hmac_sha512_update(&ctx, seq_be, 4);
75 ssh_hmac_sha512_update(&ctx, buf, buf_len);
76 ssh_hmac_sha512_final(&ctx, mac_out);
77 }
78 else
79 {
80 SshHmacCtx ctx;
81 ssh_hmac_sha256_init(&ctx, mac_key, 32);
82 ssh_hmac_sha256_update(&ctx, seq_be, 4);
83 ssh_hmac_sha256_update(&ctx, buf, buf_len);
84 ssh_hmac_sha256_final(&ctx, mac_out);
85 }
86}
87
88// Constant-time 32-byte comparison to prevent timing oracles on MAC verify.
89static int ct_memcmp(const uint8_t *a, const uint8_t *b, size_t n)
90{
91 uint8_t diff = 0;
92 for (size_t i = 0; i < n; i++)
93 diff |= a[i] ^ b[i];
94 return (int)diff;
95}
96
97// ---------------------------------------------------------------------------
98// Init
99// ---------------------------------------------------------------------------
100
101void ssh_pkt_init(uint8_t i)
102{
103 if (i >= MAX_SSH_CONNS)
104 return;
105 SshPacketState *s = &ssh_pkt[i];
106 memset(s, 0, sizeof(*s));
107 s->kex_active = true;
108 s->enc_out = false;
109 s->enc_in = false;
110}
111
112// ---------------------------------------------------------------------------
113// Send
114// ---------------------------------------------------------------------------
115
116int 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)
117{
118 if (i >= MAX_SSH_CONNS)
119 return -1;
120 SshPacketState *s = &ssh_pkt[i];
121 SshKeyMat *km = &ssh_keys[i];
122
123 // Sequence overflow guard.
125 return -1;
126
127#if DETWS_ENABLE_SSH_ZLIB
128 // Compression (RFC 4253 §6.2) transforms the payload BEFORE padding/encryption, once the s2c
129 // stream is active. The compressor is stateful (context takeover), so this call must be followed
130 // by a full send - the same atomicity the stateful cipher below already requires. The wire buffer
131 // is sized (SSH_WIRE_CAP) so the compressed payload can never overflow out_cap and desync.
132 ScratchScope comp_scope;
133 if (ssh_comp_s2c_active(i))
134 {
135 size_t bound = ssh_deflate_bound(payload_len);
136 uint8_t *cbuf = (uint8_t *)scratch_alloc(bound, 16);
137 size_t clen = 0;
138 if (!cbuf || ssh_comp_s2c(i, payload, payload_len, cbuf, bound, &clen) != 0)
139 return -1;
140 payload = cbuf;
141 payload_len = clen;
142 }
143#endif
144
145 // Padding block size and base differ by mode. chacha/gcm (AEAD) and aes-ETM exclude the 4-byte
146 // length from the block-alignment (it is AAD / sent in clear); plain aes-E&M includes it.
147 // chacha : block 8, base = padding_length + payload
148 // aes GCM : block 16, base = padding_length + payload (RFC 5647 sec 7.3)
149 // aes ETM : block 16, base = padding_length + payload
150 // aes E&M / plaintext : block 16, base = length + padding_length + payload (compute_padding)
151 bool chacha = s->enc_out && km->cipher_mode == SSH_CIPHER_CHACHA20POLY1305;
152 bool gcm = s->enc_out && km->cipher_mode == SSH_CIPHER_AES256GCM;
153 bool etm = s->enc_out && km->cipher_mode == SSH_CIPHER_AES256CTR && ssh_mac_is_etm(km->mac_mode);
154 size_t pad_len;
155 size_t tag_len;
156 if (chacha)
157 {
158 size_t base = 1 + payload_len;
159 pad_len = 8 - (base % 8);
160 if (pad_len < 4)
161 pad_len += 8;
162 tag_len = SSH_CHACHAPOLY_TAG_LEN;
163 }
164 else if (gcm)
165 {
166 size_t base = 1 + payload_len;
167 pad_len = 16 - (base % 16);
168 if (pad_len < 4)
169 pad_len += 16;
170 tag_len = SSH_AESGCM_TAG_LEN;
171 }
172 else if (etm)
173 {
174 size_t base = 1 + payload_len;
175 pad_len = 16 - (base % 16);
176 if (pad_len < 4)
177 pad_len += 16;
178 tag_len = ssh_mac_len(km->mac_mode);
179 }
180 else
181 {
182 pad_len = compute_padding(payload_len);
183 tag_len = s->enc_out ? ssh_mac_len(km->mac_mode) : 0;
184 }
185 size_t pkt_len = 1 + payload_len + pad_len; // padding_length + payload + padding
186 size_t wire_len = 4 + pkt_len + tag_len;
187
188 if (wire_len > out_cap)
189 return -1;
190
191 // Assemble the plaintext packet into out[].
192 write_u32_be(out, (uint32_t)pkt_len); // packet_length
193 out[4] = (uint8_t)pad_len; // padding_length
194 memcpy(out + 5, payload, payload_len); // payload
195 esp_fill_random(out + 5 + payload_len, pad_len); // random padding
196
197 if (chacha)
198 {
199 // Encrypt length (header key) + payload (main key) and append the Poly1305 tag.
200 ssh_chachapoly_encrypt(km->chacha_key_s2c, s->seq_no_send, out, out, (uint32_t)pkt_len);
201 }
202 else if (gcm)
203 {
204 // aes256-gcm@openssh.com: length stays in clear (it is the AAD); seal the packet body in
205 // place and append the 16-byte GCM tag. The context's invocation counter advances by one.
206 ssh_aesgcm_seal(&km->gcm_s2c, out, 4, out + 4, pkt_len, out + 4);
207 }
208 else if (etm)
209 {
210 // Encrypt-then-MAC: length stays in clear; encrypt the payload, then MAC over (length||ct).
211 ssh_aes256ctr_crypt(&km->s2c_ctx, out + 4, out + 4, pkt_len);
212 compute_mac_mode(km->mac_mode, km->mac_key_s2c, s->seq_no_send, out, 4 + pkt_len, out + 4 + pkt_len);
213 }
214 else if (s->enc_out)
215 {
216 // Encrypt-and-MAC: MAC over plaintext (seq || unencrypted packet), then AES-256-CTR.
217 uint8_t mac[64];
218 compute_mac_mode(km->mac_mode, km->mac_key_s2c, s->seq_no_send, out, 4 + pkt_len, mac);
219 ssh_aes256ctr_crypt(&km->s2c_ctx, out, out, 4 + pkt_len);
220 memcpy(out + 4 + pkt_len, mac, tag_len);
221 ssh_wipe(mac, sizeof(mac));
222 }
223
224 *out_len = wire_len;
225 s->seq_no_send++;
226 return 0;
227}
228
229// ---------------------------------------------------------------------------
230// Receive
231// ---------------------------------------------------------------------------
232
233int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler)
234{
235 if (i >= MAX_SSH_CONNS)
236 return -1;
237 SshPacketState *s = &ssh_pkt[i];
238 SshKeyMat *km = &ssh_keys[i];
239
240 // Append to receive buffer.
241 size_t space = SSH_PKT_BUF_SIZE - s->rx_len;
242 if (len > space)
243 {
244 // Buffer overflow - discard and disconnect.
245 ssh_wipe(s->rx_buf, s->rx_len);
246 s->rx_len = 0;
247 return -1;
248 }
249 memcpy(s->rx_buf + s->rx_len, data, len);
250 s->rx_len += len;
251
252 // Extract complete packets.
253 while (s->rx_len >= 4)
254 {
256 {
257 // chacha20-poly1305@openssh.com. Keyed by the sequence number, so decrypting the
258 // length is stateless/repeatable - no cipher-state peek/restore is needed.
259 if (s->rx_len < 4) // GCOVR_EXCL_LINE the enclosing while already requires rx_len >= 4
260 break; // GCOVR_EXCL_LINE (defensive re-check)
261
262 uint32_t pkt_len = ssh_chachapoly_get_length(km->chacha_key_c2s, s->seq_no_recv, s->rx_buf);
263 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 - SSH_CHACHAPOLY_TAG_LEN)
264 {
265 ssh_wipe(s->rx_buf, s->rx_len);
266 s->rx_len = 0;
267 return -1;
268 }
269 size_t wire_need = 4 + pkt_len + SSH_CHACHAPOLY_TAG_LEN;
270 if (s->rx_len < wire_need)
271 break; // incomplete packet
272
273 const size_t scratch_sz = 4 + pkt_len; // plaintext = length(4) || (pad_len||payload||pad)
274 ScratchScope scratch_scope;
275 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
276 if (!scratch)
277 {
278 ssh_wipe(s->rx_buf, s->rx_len);
279 s->rx_len = 0;
280 return -1;
281 }
282
283 // Verify the Poly1305 tag over the ciphertext, then decrypt. No plaintext on failure.
284 if (!ssh_chachapoly_decrypt(km->chacha_key_c2s, s->seq_no_recv, scratch, s->rx_buf, pkt_len))
285 {
286 ssh_wipe(scratch, scratch_sz);
287 ssh_wipe(s->rx_buf, s->rx_len);
288 s->rx_len = 0;
289 return -1; // caller must close connection
290 }
291
293 {
294 ssh_wipe(scratch, scratch_sz);
295 return -1;
296 }
297 s->seq_no_recv++;
298
299 uint8_t pad_len_byte = scratch[4];
300 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
301 {
302 ssh_wipe(scratch, scratch_sz);
303 return -1;
304 }
305 size_t payload_len = pkt_len - 1 - pad_len_byte;
306 uint8_t msg_type = scratch[5];
307 handler(i, msg_type, scratch + 5, payload_len);
308
309 size_t consumed = wire_need;
310 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
311 s->rx_len -= consumed;
312 ssh_wipe(scratch, scratch_sz);
313 }
314 else if (s->enc_in && km->cipher_mode == SSH_CIPHER_AES256GCM)
315 {
316 // aes256-gcm@openssh.com (RFC 5647): the 4-byte packet_length is sent in the clear and is
317 // the AEAD's additional authenticated data; the 16-byte GCM tag is verified over
318 // (length || ciphertext) BEFORE any plaintext is produced.
319 if (s->rx_len < 4)
320 break;
321 uint32_t pkt_len = read_u32_be(s->rx_buf);
322 // The encrypted portion (pkt_len) must be a positive whole number of AES blocks.
323 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 - SSH_AESGCM_TAG_LEN || (pkt_len % 16) != 0)
324 {
325 ssh_wipe(s->rx_buf, s->rx_len);
326 s->rx_len = 0;
327 return -1;
328 }
329 size_t wire_need = 4 + pkt_len + SSH_AESGCM_TAG_LEN;
330 if (s->rx_len < wire_need)
331 break; // incomplete packet
332
333 const size_t scratch_sz = pkt_len; // plaintext = padding_length || payload || padding
334 ScratchScope scratch_scope;
335 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
336 if (!scratch)
337 {
338 ssh_wipe(s->rx_buf, s->rx_len);
339 s->rx_len = 0;
340 return -1;
341 }
342
343 // Verify the GCM tag over (length || ciphertext), then decrypt. No plaintext on failure.
344 if (!ssh_aesgcm_open(&km->gcm_c2s, s->rx_buf, 4, s->rx_buf + 4, pkt_len, s->rx_buf + 4 + pkt_len, scratch))
345 {
346 ssh_wipe(scratch, scratch_sz);
347 ssh_wipe(s->rx_buf, s->rx_len);
348 s->rx_len = 0;
349 return -1; // caller must close connection
350 }
351
353 {
354 ssh_wipe(scratch, scratch_sz);
355 return -1;
356 }
357 s->seq_no_recv++;
358
359 uint8_t pad_len_byte = scratch[0];
360 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
361 {
362 ssh_wipe(scratch, scratch_sz);
363 return -1;
364 }
365 size_t payload_len = pkt_len - 1 - pad_len_byte;
366 uint8_t msg_type = scratch[1];
367 handler(i, msg_type, scratch + 1, payload_len);
368
369 size_t consumed = wire_need;
370 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
371 s->rx_len -= consumed;
372 ssh_wipe(scratch, scratch_sz);
373 }
374 else if (s->enc_in && ssh_mac_is_etm(km->mac_mode))
375 {
376 // aes256-ctr + encrypt-then-MAC: the 4-byte packet_length is sent in the clear, and the
377 // MAC is verified over (length || ciphertext) BEFORE anything is decrypted.
378 if (s->rx_len < 4)
379 break;
380 uint32_t pkt_len = read_u32_be(s->rx_buf);
381 size_t mac_tag = ssh_mac_len(km->mac_mode);
382 // The encrypted portion (pkt_len) must be a positive whole number of AES blocks.
383 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 || (pkt_len % 16) != 0)
384 {
385 ssh_wipe(s->rx_buf, s->rx_len);
386 s->rx_len = 0;
387 return -1;
388 }
389 size_t wire_need = 4 + pkt_len + mac_tag;
390 if (s->rx_len < wire_need)
391 break; // incomplete packet
392
393 uint8_t expected_mac[64];
394 compute_mac_mode(km->mac_mode, km->mac_key_c2s, s->seq_no_recv, s->rx_buf, 4 + pkt_len, expected_mac);
395 if (ct_memcmp(expected_mac, s->rx_buf + 4 + pkt_len, mac_tag) != 0)
396 {
397 ssh_wipe(expected_mac, sizeof(expected_mac));
398 ssh_wipe(s->rx_buf, s->rx_len);
399 s->rx_len = 0;
400 return -1; // caller must close connection
401 }
402 ssh_wipe(expected_mac, sizeof(expected_mac));
403
405 return -1;
406 s->seq_no_recv++;
407
408 // MAC verified -> decrypt the payload (advances c2s_ctx by exactly pkt_len/16 blocks).
409 const size_t scratch_sz = SSH_PKT_BUF_SIZE;
410 ScratchScope scratch_scope;
411 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
412 if (!scratch)
413 {
414 ssh_wipe(s->rx_buf, s->rx_len);
415 s->rx_len = 0;
416 return -1;
417 }
418 memcpy(scratch, s->rx_buf + 4, pkt_len);
419 ssh_aes256ctr_crypt(&km->c2s_ctx, scratch, scratch, pkt_len);
420
421 // scratch = padding_length || payload || padding.
422 uint8_t pad_len_byte = scratch[0];
423 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
424 {
425 ssh_wipe(scratch, scratch_sz);
426 return -1;
427 }
428 size_t payload_len = pkt_len - 1 - pad_len_byte;
429 uint8_t msg_type = scratch[1];
430 handler(i, msg_type, scratch + 1, payload_len);
431
432 size_t consumed = wire_need;
433 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
434 s->rx_len -= consumed;
435 ssh_wipe(scratch, scratch_sz);
436 }
437 else if (s->enc_in)
438 {
439 // aes256-ctr + encrypt-and-MAC. We need the first cipher block (16 bytes) for the length.
440 if (s->rx_len < 16)
441 break; // wait for more data
442
443 // --- Peek packet_length WITHOUT permanently advancing the cipher ---
444 // AES-CTR is stateful: decrypting bytes advances the counter. To
445 // read the length without committing, snapshot the CTR streaming
446 // state (counter/keystream/pos - the key schedule is invariant and
447 // may hold internal pointers on mbedtls, so we never copy it),
448 // decrypt the first block, then restore. Nothing is consumed yet.
449 uint8_t saved_counter[16];
450 uint8_t saved_keystream[16];
451 uint8_t saved_pos;
452 memcpy(saved_counter, km->c2s_ctx.counter, 16);
453 memcpy(saved_keystream, km->c2s_ctx.keystream, 16);
454 saved_pos = km->c2s_ctx.pos;
455
456 uint8_t len_block[16];
457 memcpy(len_block, s->rx_buf, 16);
458 ssh_aes256ctr_crypt(&km->c2s_ctx, len_block, len_block, 16);
459 uint32_t pkt_len = read_u32_be(len_block);
460 ssh_wipe(len_block, sizeof(len_block));
461
462 // Restore the cipher to the packet boundary (un-peek).
463 memcpy(km->c2s_ctx.counter, saved_counter, 16);
464 memcpy(km->c2s_ctx.keystream, saved_keystream, 16);
465 km->c2s_ctx.pos = saved_pos;
466 ssh_wipe(saved_keystream, sizeof(saved_keystream));
467
468 // Validate length. The encrypted portion (4 + pkt_len) must be a
469 // whole number of AES blocks (RFC 4253 §6 padding guarantees this).
470 size_t enc_len = 4 + pkt_len;
471 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 || (enc_len % 16) != 0)
472 {
473 ssh_wipe(s->rx_buf, s->rx_len);
474 s->rx_len = 0;
475 return -1;
476 }
477
478 size_t mac_tag = ssh_mac_len(km->mac_mode);
479 size_t wire_need = enc_len + mac_tag;
480 if (s->rx_len < wire_need)
481 break; // incomplete packet; cipher state already restored
482
483 // Borrow this packet's plaintext scratch from the shared arena. The
484 // scope guard reclaims it on every exit path, so multiple packets in
485 // one call reuse the same space instead of accumulating; an exhausted
486 // arena fails closed (discard + disconnect).
487 const size_t scratch_sz = SSH_PKT_BUF_SIZE + 64;
488 ScratchScope scratch_scope;
489 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
490 if (!scratch)
491 {
492 ssh_wipe(s->rx_buf, s->rx_len);
493 s->rx_len = 0;
494 return -1;
495 }
496
497 // Full packet present. Decrypt EXACTLY the encrypted portion,
498 // which advances c2s_ctx by exactly enc_len/16 blocks and leaves
499 // the cipher aligned on the next packet boundary.
500 memcpy(scratch, s->rx_buf, enc_len);
501 ssh_aes256ctr_crypt(&km->c2s_ctx, scratch, scratch, enc_len);
502
503 // Verify MAC over seq_no || plaintext(scratch[0..enc_len)).
504 const uint8_t *rx_mac = s->rx_buf + enc_len; // MAC is sent in clear
505 uint8_t expected_mac[64];
506 compute_mac_mode(km->mac_mode, km->mac_key_c2s, s->seq_no_recv, scratch, enc_len, expected_mac);
507
508 if (ct_memcmp(expected_mac, rx_mac, mac_tag) != 0)
509 {
510 // MAC failure: zero everything and disconnect.
511 ssh_wipe(scratch, scratch_sz);
512 ssh_wipe(expected_mac, sizeof(expected_mac));
513 ssh_wipe(s->rx_buf, s->rx_len);
514 s->rx_len = 0;
515 return -1; // caller must close connection
516 }
517 ssh_wipe(expected_mac, sizeof(expected_mac));
518
519 // MAC verified. Sequence overflow guard.
521 {
522 ssh_wipe(scratch, scratch_sz);
523 return -1;
524 }
525 s->seq_no_recv++;
526
527 // Extract payload: scratch[5 .. 5 + payload_len - 1]
528 uint8_t pad_len_byte = scratch[4];
529 // RFC 4253 6: there MUST be at least 4 bytes of padding, and it cannot
530 // exceed the packet (which would underflow payload_len).
531 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
532 {
533 ssh_wipe(scratch, scratch_sz);
534 return -1;
535 }
536 size_t payload_len = pkt_len - 1 - pad_len_byte;
537 uint8_t msg_type = scratch[5];
538 handler(i, msg_type, scratch + 5, payload_len);
539
540 // Consume from rx_buf.
541 size_t consumed = wire_need;
542 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
543 s->rx_len -= consumed;
544 ssh_wipe(scratch, scratch_sz);
545 }
546 else
547 {
548 // Unencrypted path (during initial handshake / before NEWKEYS).
549 uint32_t pkt_len = read_u32_be(s->rx_buf);
550 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4)
551 {
552 ssh_wipe(s->rx_buf, s->rx_len);
553 s->rx_len = 0;
554 return -1;
555 }
556 size_t wire_need = 4 + pkt_len; // no MAC before NEWKEYS
557 if (s->rx_len < wire_need)
558 break;
559
561 return -1;
562 s->seq_no_recv++;
563
564 uint8_t pad_len_byte = s->rx_buf[4];
565 if (pad_len_byte >= pkt_len)
566 return -1;
567 size_t payload_len = pkt_len - 1 - pad_len_byte;
568 uint8_t msg_type = s->rx_buf[5];
569 handler(i, msg_type, s->rx_buf + 5, payload_len);
570
571 size_t consumed = wire_need;
572 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
573 s->rx_len -= consumed;
574 }
575 }
576
577 return 0;
578}
579
580// ---------------------------------------------------------------------------
581// Disconnect
582// ---------------------------------------------------------------------------
583
584int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap)
585{
586 if (i >= MAX_SSH_CONNS)
587 return -1;
588
589 // Build SSH_MSG_DISCONNECT payload (RFC 4253 §11.1):
590 // byte SSH_MSG_DISCONNECT
591 // uint32 reason code
592 // string description (empty)
593 // string language tag (empty)
594 uint8_t payload[13];
595 payload[0] = SSH_MSG_DISCONNECT;
596 payload[1] = (uint8_t)(reason_code >> 24);
597 payload[2] = (uint8_t)(reason_code >> 16);
598 payload[3] = (uint8_t)(reason_code >> 8);
599 payload[4] = (uint8_t)(reason_code);
600 payload[5] = 0;
601 payload[6] = 0;
602 payload[7] = 0;
603 payload[8] = 0; // empty description
604 payload[9] = 0;
605 payload[10] = 0;
606 payload[11] = 0;
607 payload[12] = 0; // empty language
608
609 int rc = ssh_pkt_send(i, payload, sizeof(payload), out, out_len, out_cap);
610
611 // Zero packet state and key material regardless of send success.
612 ssh_pkt_init(i);
613 ssh_keymat_wipe(i);
614 ssh_dh_wipe(i);
615
616 return rc;
617}
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
RAII scope guard for transient scratch borrows.
Definition scratch.h:110
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:85
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
void ssh_aes256ctr_crypt(SshAesCtrCtx *ctx, const uint8_t *in, uint8_t *out, size_t len)
Encrypt or decrypt len bytes in-place (or src→dst).
void ssh_aesgcm_seal(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *out)
Seal one packet: AES-256-GCM encrypt pt (pt_len bytes) and authenticate it together with aad....
bool ssh_aesgcm_open(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[SSH_AESGCM_TAG_LEN], uint8_t *out)
Open one packet: verify the 16-byte tag over aad || ct in constant time, and only on success decrypt ...
AES-256-GCM AEAD for SSH (aes256-gcm@openssh.com, RFC 5647).
void ssh_chachapoly_encrypt(const uint8_t key[SSH_CHACHAPOLY_KEY_LEN], uint32_t seqnr, uint8_t *dest, const uint8_t *src, uint32_t payload_len)
Encrypt+authenticate one packet.
uint32_t ssh_chachapoly_get_length(const uint8_t key[SSH_CHACHAPOLY_KEY_LEN], uint32_t seqnr, const uint8_t enc_len[SSH_CHACHAPOLY_AAD_LEN])
Decrypt just the 4-byte length field to learn the packet length before reading the body.
bool ssh_chachapoly_decrypt(const uint8_t key[SSH_CHACHAPOLY_KEY_LEN], uint32_t seqnr, uint8_t *dest, const uint8_t *src, uint32_t payload_len)
Verify+decrypt one packet.
chacha20-poly1305@openssh.com AEAD cipher (OpenSSH PROTOCOL.chacha20poly1305).
#define SSH_CHACHAPOLY_TAG_LEN
Poly1305 tag.
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
void ssh_hmac_sha256_final(SshHmacCtx *ctx, uint8_t mac[SSH_HMAC_SHA256_LEN])
Finalize and write the 32-byte MAC.
void ssh_hmac_sha256_init(SshHmacCtx *ctx, const uint8_t *key, size_t key_len)
Initialize a streaming HMAC-SHA2-256 context.
void ssh_hmac_sha256_update(SshHmacCtx *ctx, const uint8_t *data, size_t len)
Feed len bytes into the running HMAC.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
void ssh_hmac_sha512_final(SshHmacSha512Ctx *ctx, uint8_t mac[SSH_HMAC_SHA512_LEN])
Finish, writing the 64-byte MAC.
void ssh_hmac_sha512_init(SshHmacSha512Ctx *ctx, const uint8_t *key, size_t key_len)
Begin an HMAC-SHA2-512 over key (keys > 128 bytes are pre-hashed per RFC 2104).
void ssh_hmac_sha512_update(SshHmacSha512Ctx *ctx, const uint8_t *data, size_t len)
Feed len message bytes.
HMAC-SHA2-512 (RFC 2104 + FIPS 198-1).
SshKeyMat ssh_keys[MAX_SSH_CONNS]
Pool of session key material, one entry per MAX_SSH_CONNS.
SSH session key material - types, pools, and security model.
@ SSH_CIPHER_CHACHA20POLY1305
chacha20-poly1305@openssh.com (AEAD; no separate MAC)
Definition ssh_keymat.h:119
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
Definition ssh_keymat.h:120
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
Definition ssh_keymat.h:118
@ SSH_MAC_HMAC_SHA512
hmac-sha2-512 (encrypt-and-MAC)
Definition ssh_keymat.h:127
@ SSH_MAC_HMAC_SHA512_ETM
hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
Definition ssh_keymat.h:129
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.
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_SEQ_CLOSE_THRESHOLD
Close the connection when seq_no reaches this value.
Definition ssh_packet.h:90
#define SSH_MSG_DISCONNECT
Definition ssh_packet.h:96
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
SSH server-to-client compression: a context-takeover DEFLATE stream (no heap).
uint8_t counter[16]
Current CTR block (big-endian 128-bit counter).
uint8_t pos
Next byte position within keystream[].
uint8_t keystream[16]
Buffered keystream from last AES-ECB call.
Streaming HMAC-SHA2-256 context.
Streaming HMAC-SHA2-512 context (stores the opad key block + inner hash state).
AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
Definition ssh_keymat.h:192
SshAesGcmCtx gcm_s2c
server-to-client AES-256-GCM (server seals outbound with it).
Definition ssh_keymat.h:208
uint8_t mac_key_c2s[64]
HMAC key, client-to-server (aes mode); 32 bytes for SHA-256, 64 for SHA-512.
Definition ssh_keymat.h:196
SshAesCtrCtx c2s_ctx
Client→server cipher (AES-256-CTR); server decrypts inbound with it.
Definition ssh_keymat.h:193
SshAesGcmCtx gcm_c2s
client-to-server AES-256-GCM (server opens inbound with it).
Definition ssh_keymat.h:207
uint8_t cipher_mode
SSH_CIPHER_* selected for this session (0 = aes256-ctr).
Definition ssh_keymat.h:200
uint8_t mac_key_s2c[64]
HMAC key, server-to-client (aes mode).
Definition ssh_keymat.h:197
uint8_t chacha_key_c2s[SSH_CHACHAPOLY_KEY_LEN]
client-to-server, used only in chacha mode.
Definition ssh_keymat.h:202
uint8_t chacha_key_s2c[SSH_CHACHAPOLY_KEY_LEN]
server-to-client, used only in chacha mode.
Definition ssh_keymat.h:203
uint8_t mac_mode
SSH_MAC_* selected for the aes256-ctr cipher (0 = hmac-sha2-256 E&M).
Definition ssh_keymat.h:198
SshAesCtrCtx s2c_ctx
Server→client cipher (AES-256-CTR); server encrypts outbound with it.
Definition ssh_keymat.h:194
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