ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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
10#include "crypto/aead/aesgcm.h"
15#if PC_ENABLE_SSH_ZLIB
18#endif
19#include "server/mmgr/scratch.h"
20#include "server/mmgr/secure.h"
21#include <string.h>
22
23#ifdef ARDUINO
24#include <Arduino.h> // esp_fill_random()
25#else
26#include <Arduino.h> // mock
27#endif
28
29// ---------------------------------------------------------------------------
30// BSS allocation
31// ---------------------------------------------------------------------------
32
34
35// ---------------------------------------------------------------------------
36// Internal helpers
37// ---------------------------------------------------------------------------
38
39static inline uint32_t read_u32_be(const uint8_t *p)
40{
41 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
42}
43
44static inline void write_u32_be(uint8_t *p, uint32_t v)
45{
46 p[0] = (uint8_t)(v >> 24);
47 p[1] = (uint8_t)(v >> 16);
48 p[2] = (uint8_t)(v >> 8);
49 p[3] = (uint8_t)(v);
50}
51
52// Compute the padding needed so that (5 + payload_len + padding) is a
53// multiple of 16 (AES block size). Minimum padding = 4 bytes (RFC 4253 §6).
54static size_t compute_padding(size_t payload_len)
55{
56 size_t total = 5 + payload_len; // 4-byte length + 1-byte padding_len + payload
57 size_t remainder = total % 16;
58 size_t padding = (remainder == 0) ? 0 : (16 - remainder);
59 if (padding < 4)
60 {
61 padding += 16;
62 }
63 return padding;
64}
65
66// Compute the MAC over 4-byte seq_no || buf using the HMAC named by mac_mode. For E&M the buf is
67// the plaintext packet; for ETM it is the length field || ciphertext. Writes ssh_mac_len() bytes.
68static void compute_mac_mode(uint8_t mac_mode, const uint8_t *mac_key, uint32_t seq_no, const uint8_t *buf,
69 size_t buf_len, uint8_t *mac_out)
70{
71 uint8_t seq_be[4];
72 write_u32_be(seq_be, seq_no);
73 if (mac_mode == SSH_MAC_HMAC_SHA512 || mac_mode == SSH_MAC_HMAC_SHA512_ETM)
74 {
76 pc_hmac_sha512_init(&ctx, mac_key, 64);
77 pc_hmac_sha512_update(&ctx, seq_be, 4);
78 pc_hmac_sha512_update(&ctx, buf, buf_len);
79 pc_hmac_sha512_final(&ctx, mac_out);
80 }
81 else
82 {
84 pc_hmac_sha256_init(&ctx, mac_key, 32);
85 pc_hmac_sha256_update(&ctx, seq_be, 4);
86 pc_hmac_sha256_update(&ctx, buf, buf_len);
87 pc_hmac_sha256_final(&ctx, mac_out);
88 }
89}
90
91// Constant-time 32-byte comparison to prevent timing oracles on MAC verify.
92static int ct_memcmp(const uint8_t *a, const uint8_t *b, size_t n)
93{
94 uint8_t diff = 0;
95 for (size_t i = 0; i < n; i++)
96 {
97 diff |= a[i] ^ b[i];
98 }
99 return (int)diff;
100}
101
102// ---------------------------------------------------------------------------
103// Init
104// ---------------------------------------------------------------------------
105
106void ssh_pkt_init(uint8_t i)
107{
108 if (i >= MAX_SSH_CONNS)
109 {
110 return;
111 }
112 SshPacketState *s = &ssh_pkt[i];
113 memset(s, 0, sizeof(*s)); // is_client defaults false = server role
114 s->kex_active = true;
115 s->enc_out = false;
116 s->enc_in = false;
117}
118
119void ssh_pkt_set_client(uint8_t i)
120{
121 if (i < MAX_SSH_CONNS)
122 {
123 ssh_pkt[i].is_client = true;
124 }
125}
126
127// ---------------------------------------------------------------------------
128// Direction-aware key selection (RFC 4253 §7.2 names keys by direction, not role)
129// ---------------------------------------------------------------------------
130// A client sends c2s / receives s2c; a server is the mirror. Selecting the key set through these
131// keeps one send and one receive implementation correct for both roles.
132
133static inline const uint8_t *km_send_chacha(const SshKeyMat *km, bool cli)
134{
135 return cli ? km->chacha_key_c2s : km->chacha_key_s2c;
136}
137static inline const uint8_t *km_recv_chacha(const SshKeyMat *km, bool cli)
138{
139 return cli ? km->chacha_key_s2c : km->chacha_key_c2s;
140}
141static inline const uint8_t *km_send_aes_key(const SshKeyMat *km, bool cli)
142{
143 return cli ? km->aes_key_c2s : km->aes_key_s2c;
144}
145static inline uint8_t *km_send_aes_iv(SshKeyMat *km, bool cli)
146{
147 return cli ? km->aes_iv_c2s : km->aes_iv_s2c;
148}
149static inline const uint8_t *km_recv_aes_key(const SshKeyMat *km, bool cli)
150{
151 return cli ? km->aes_key_s2c : km->aes_key_c2s;
152}
153// GCM keeps a keyed context per direction rather than a raw key: the schedule is built once at install
154// (ssh_dh.cpp) and reused per packet, because standing one up costs ~9,200 cycles regardless of packet
155// size and would otherwise dominate small interactive traffic.
156static inline pc_aesgcm_key *km_send_gcm(SshKeyMat *km, bool cli)
157{
158 return reinterpret_cast<pc_aesgcm_key *>(cli ? km->gcm_ctx_c2s : km->gcm_ctx_s2c);
159}
160static inline pc_aesgcm_key *km_recv_gcm(SshKeyMat *km, bool cli)
161{
162 return reinterpret_cast<pc_aesgcm_key *>(cli ? km->gcm_ctx_s2c : km->gcm_ctx_c2s);
163}
164static inline uint8_t *km_recv_aes_iv(SshKeyMat *km, bool cli)
165{
166 return cli ? km->aes_iv_s2c : km->aes_iv_c2s;
167}
168static inline const uint8_t *km_send_mac(const SshKeyMat *km, bool cli)
169{
170 return cli ? km->mac_key_c2s : km->mac_key_s2c;
171}
172static inline const uint8_t *km_recv_mac(const SshKeyMat *km, bool cli)
173{
174 return cli ? km->mac_key_s2c : km->mac_key_c2s;
175}
176
177// ---------------------------------------------------------------------------
178// Send
179// ---------------------------------------------------------------------------
180
181int 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)
182{
183 if (i >= MAX_SSH_CONNS)
184 {
185 return -1;
186 }
187 SshPacketState *s = &ssh_pkt[i];
188 SshKeyMat *km = &ssh_keys[i];
189
190 // Sequence overflow guard.
192 {
193 return -1;
194 }
195
196#if PC_ENABLE_SSH_ZLIB
197 // Compression (RFC 4253 §6.2) transforms the payload BEFORE padding/encryption, once the s2c
198 // stream is active. The compressor is stateful (context takeover), so this call must be followed
199 // by a full send - the same atomicity the stateful cipher below already requires. The wire buffer
200 // is sized (SSH_WIRE_CAP) so the compressed payload can never overflow out_cap and desync.
201 ScratchScope comp_scope;
202 if (ssh_comp_s2c_active(i))
203 {
204 size_t bound = ssh_deflate_bound(payload_len);
205 uint8_t *cbuf = (uint8_t *)scratch_alloc(bound, 16);
206 size_t clen = 0;
207 if (!cbuf || ssh_comp_s2c(i, payload, payload_len, cbuf, bound, &clen) != 0)
208 {
209 return -1;
210 }
211 payload = cbuf;
212 payload_len = clen;
213 }
214#endif
215
216 // Padding block size and base differ by mode. chacha/gcm (AEAD) and aes-ETM exclude the 4-byte
217 // length from the block-alignment (it is AAD / sent in clear); plain aes-E&M includes it.
218 // chacha : block 8, base = padding_length + payload
219 // aes GCM : block 16, base = padding_length + payload (RFC 5647 sec 7.3)
220 // aes ETM : block 16, base = padding_length + payload
221 // aes E&M / plaintext : block 16, base = length + padding_length + payload (compute_padding)
222 bool chacha = s->enc_out && km->cipher_mode == SSH_CIPHER_CHACHA20POLY1305;
223 bool gcm = s->enc_out && km->cipher_mode == SSH_CIPHER_AES256GCM;
224 bool etm = s->enc_out && km->cipher_mode == SSH_CIPHER_AES256CTR && ssh_mac_is_etm(km->mac_mode);
225 size_t pad_len;
226 size_t tag_len;
227 if (chacha)
228 {
229 size_t base = 1 + payload_len;
230 pad_len = 8 - (base % 8);
231 if (pad_len < 4)
232 {
233 pad_len += 8;
234 }
235 tag_len = PC_CHACHAPOLY_TAG_LEN;
236 }
237 else if (gcm)
238 {
239 size_t base = 1 + payload_len;
240 pad_len = 16 - (base % 16);
241 if (pad_len < 4)
242 {
243 pad_len += 16;
244 }
245 tag_len = PC_AESGCM_TAG_LEN;
246 }
247 else if (etm)
248 {
249 size_t base = 1 + payload_len;
250 pad_len = 16 - (base % 16);
251 if (pad_len < 4)
252 {
253 pad_len += 16;
254 }
255 tag_len = ssh_mac_len(km->mac_mode);
256 }
257 else
258 {
259 pad_len = compute_padding(payload_len);
260 tag_len = s->enc_out ? ssh_mac_len(km->mac_mode) : 0;
261 }
262 size_t pkt_len = 1 + payload_len + pad_len; // padding_length + payload + padding
263 size_t wire_len = 4 + pkt_len + tag_len;
264
265 if (wire_len > out_cap)
266 {
267 return -1;
268 }
269
270 // Assemble the plaintext packet into out[].
271 write_u32_be(out, (uint32_t)pkt_len); // packet_length
272 out[4] = (uint8_t)pad_len; // padding_length
273 memcpy(out + 5, payload, payload_len); // payload
274 esp_fill_random(out + 5 + payload_len, pad_len); // random padding
275
276 const bool cli = s->is_client; // send direction: client uses c2s, server uses s2c
277 if (chacha)
278 {
279 // Encrypt length (header key) + payload (main key) and append the Poly1305 tag.
280 pc_chachapoly_encrypt(km_send_chacha(km, cli), s->seq_no_send, out, out, (uint32_t)pkt_len);
281 }
282 else if (gcm)
283 {
284 // aes256-gcm@openssh.com: length stays in clear (it is the AAD); seal the packet body in
285 // place and append the 16-byte GCM tag. The context's invocation counter advances by one.
286 // Seal in place (tag appended after the ciphertext), then advance the RFC 5647 invocation counter.
287 uint8_t *iv = km_send_aes_iv(km, cli);
288 pc_aesgcm_seal(km_send_gcm(km, cli), iv, out, 4, out + 4, pkt_len, out + 4, out + 4 + pkt_len);
290 }
291 else if (etm)
292 {
293 // Encrypt-then-MAC: length stays in clear; encrypt the payload, then MAC over (length||ct).
294 pc_aes256ctr_crypt(km_send_aes_key(km, cli), km_send_aes_iv(km, cli), out + 4, out + 4, pkt_len);
295 compute_mac_mode(km->mac_mode, km_send_mac(km, cli), s->seq_no_send, out, 4 + pkt_len, out + 4 + pkt_len);
296 }
297 else if (s->enc_out)
298 {
299 // Encrypt-and-MAC: MAC over plaintext (seq || unencrypted packet), then AES-256-CTR.
300 uint8_t mac[64];
301 compute_mac_mode(km->mac_mode, km_send_mac(km, cli), s->seq_no_send, out, 4 + pkt_len, mac);
302 pc_aes256ctr_crypt(km_send_aes_key(km, cli), km_send_aes_iv(km, cli), out, out, 4 + pkt_len);
303 memcpy(out + 4 + pkt_len, mac, tag_len);
304 pc_secure_wipe(mac, sizeof(mac));
305 }
306
307 *out_len = wire_len;
308 s->seq_no_send++;
309 return 0;
310}
311
312// ---------------------------------------------------------------------------
313// Receive
314// ---------------------------------------------------------------------------
315
316// Dispatch one decrypted packet payload (message-type byte + data) to @p handler, first decompressing
317// it when the client-to-server compression stream is active (RFC 4253 sec 6.2). Compression only runs
318// after NEWKEYS / auth success, so the pre-auth plaintext path never enters the c2s branch.
319// @return 0 on success (handler invoked, or skipped for a flush-only packet), -1 on a malformed
320// compressed stream / decompression overflow (the caller must wipe + disconnect).
321static int ssh_dispatch_payload(uint8_t i, const uint8_t *payload, size_t payload_len, ssh_msg_handler_t handler)
322{
323#if PC_ENABLE_SSH_ZLIB
324 if (ssh_comp_c2s_active(i))
325 {
326 ScratchScope inflate_scope;
327 uint8_t *dbuf = (uint8_t *)scratch_alloc(SSH_PKT_BUF_SIZE, 16);
328 size_t dlen = 0;
329 if (!dbuf || ssh_comp_c2s(i, payload, payload_len, dbuf, SSH_PKT_BUF_SIZE, &dlen) != 0)
330 {
331 return -1; // malformed stream, or a payload that decompresses beyond the uncompressed limit
332 }
333 if (dlen == 0)
334 {
335 return 0; // the packet carried only flush bits (no message); consume it and move on
336 }
337 handler(i, dbuf[0], dbuf, dlen);
338 return 0;
339 }
340#endif
341 handler(i, payload[0], payload, payload_len);
342 return 0;
343}
344
345// Extract one packet from the head of the RX buffer and dispatch it to @p handler. Every cipher path
346// returns the same tri-state so ssh_pkt_recv's extract loop can stay flat: 1 = one packet consumed (keep
347// extracting), 0 = incomplete (need more bytes, stop), -1 = fatal (the buffer is already wiped on the paths
348// that require it; the caller must disconnect). One function per cipher mode keeps each within the nesting
349// and cognitive-complexity budget that the single inline switch blew past.
350
351static int ssh_recv_chachapoly(uint8_t i, SshPacketState *s, const SshKeyMat *km, ssh_msg_handler_t handler)
352{
353 // chacha20-poly1305@openssh.com. Keyed by the sequence number, so decrypting the
354 // length is stateless/repeatable - no cipher-state peek/restore is needed.
355 const uint8_t *rk = km_recv_chacha(km, s->is_client); // recv: client s2c, server c2s
356 uint32_t pkt_len = pc_chachapoly_get_length(rk, s->seq_no_recv, s->rx_buf);
357 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 - PC_CHACHAPOLY_TAG_LEN)
358 {
359 pc_secure_wipe(s->rx_buf, s->rx_len);
360 s->rx_len = 0;
361 return -1;
362 }
363 size_t wire_need = 4 + pkt_len + PC_CHACHAPOLY_TAG_LEN;
364 if (s->rx_len < wire_need)
365 {
366 return 0; // incomplete packet
367 }
368
369 const size_t scratch_sz = 4 + pkt_len; // plaintext = length(4) || (pad_len||payload||pad)
370 ScratchScope scratch_scope;
371 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
372 if (!scratch)
373 {
374 pc_secure_wipe(s->rx_buf, s->rx_len);
375 s->rx_len = 0;
376 return -1;
377 }
378
379 // Verify the Poly1305 tag over the ciphertext, then decrypt. No plaintext on failure.
380 if (!pc_chachapoly_decrypt(rk, s->seq_no_recv, scratch, s->rx_buf, pkt_len))
381 {
382 pc_secure_wipe(scratch, scratch_sz);
383 pc_secure_wipe(s->rx_buf, s->rx_len);
384 s->rx_len = 0;
385 return -1; // caller must close connection
386 }
387
389 {
390 pc_secure_wipe(scratch, scratch_sz);
391 return -1;
392 }
393 s->seq_no_recv++;
394
395 uint8_t pad_len_byte = scratch[4];
396 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
397 {
398 pc_secure_wipe(scratch, scratch_sz);
399 return -1;
400 }
401 size_t payload_len = pkt_len - 1 - pad_len_byte;
402 if (ssh_dispatch_payload(i, scratch + 5, payload_len, handler) < 0)
403 {
404 pc_secure_wipe(scratch, scratch_sz);
405 return -1;
406 }
407
408 size_t consumed = wire_need;
409 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
410 s->rx_len -= consumed;
411 pc_secure_wipe(scratch, scratch_sz);
412 return 1;
413}
414
415static int ssh_recv_aesgcm(uint8_t i, SshPacketState *s, SshKeyMat *km, ssh_msg_handler_t handler)
416{
417 // aes256-gcm@openssh.com (RFC 5647): the 4-byte packet_length is sent in the clear and is
418 // the AEAD's additional authenticated data; the 16-byte GCM tag is verified over
419 // (length || ciphertext) BEFORE any plaintext is produced.
420 uint32_t pkt_len = read_u32_be(s->rx_buf);
421 // The encrypted portion (pkt_len) must be a positive whole number of AES blocks.
422 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 - PC_AESGCM_TAG_LEN || (pkt_len % 16) != 0)
423 {
424 pc_secure_wipe(s->rx_buf, s->rx_len);
425 s->rx_len = 0;
426 return -1;
427 }
428 size_t wire_need = 4 + pkt_len + PC_AESGCM_TAG_LEN;
429 if (s->rx_len < wire_need)
430 {
431 return 0; // incomplete packet
432 }
433
434 const size_t scratch_sz = pkt_len; // plaintext = padding_length || payload || padding
435 ScratchScope scratch_scope;
436 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
437 if (!scratch)
438 {
439 pc_secure_wipe(s->rx_buf, s->rx_len);
440 s->rx_len = 0;
441 return -1;
442 }
443
444 // Verify the GCM tag over (length || ciphertext), then decrypt. No plaintext on failure.
445 uint8_t *iv = km_recv_aes_iv(km, s->is_client);
446 if (!pc_aesgcm_open(km_recv_gcm(km, s->is_client), iv, s->rx_buf, 4, s->rx_buf + 4, pkt_len,
447 s->rx_buf + 4 + pkt_len, scratch))
448 {
449 pc_secure_wipe(scratch, scratch_sz);
450 pc_secure_wipe(s->rx_buf, s->rx_len);
451 s->rx_len = 0;
452 return -1; // caller must close connection
453 }
454 pc_aesgcm_iv_increment(iv); // tag verified: advance the RFC 5647 invocation counter (recv success)
455
457 {
458 pc_secure_wipe(scratch, scratch_sz);
459 return -1;
460 }
461 s->seq_no_recv++;
462
463 uint8_t pad_len_byte = scratch[0];
464 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
465 {
466 pc_secure_wipe(scratch, scratch_sz);
467 return -1;
468 }
469 size_t payload_len = pkt_len - 1 - pad_len_byte;
470 if (ssh_dispatch_payload(i, scratch + 1, payload_len, handler) < 0)
471 {
472 pc_secure_wipe(scratch, scratch_sz);
473 return -1;
474 }
475
476 size_t consumed = wire_need;
477 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
478 s->rx_len -= consumed;
479 pc_secure_wipe(scratch, scratch_sz);
480 return 1;
481}
482
483static int ssh_recv_ctr_etm(uint8_t i, SshPacketState *s, SshKeyMat *km, ssh_msg_handler_t handler)
484{
485 // aes256-ctr + encrypt-then-MAC: the 4-byte packet_length is sent in the clear, and the
486 // MAC is verified over (length || ciphertext) BEFORE anything is decrypted.
487 uint32_t pkt_len = read_u32_be(s->rx_buf);
488 size_t mac_tag = ssh_mac_len(km->mac_mode);
489 // The encrypted portion (pkt_len) must be a positive whole number of AES blocks.
490 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 || (pkt_len % 16) != 0)
491 {
492 pc_secure_wipe(s->rx_buf, s->rx_len);
493 s->rx_len = 0;
494 return -1;
495 }
496 size_t wire_need = 4 + pkt_len + mac_tag;
497 if (s->rx_len < wire_need)
498 {
499 return 0; // incomplete packet
500 }
501
502 uint8_t expected_mac[64];
503 compute_mac_mode(km->mac_mode, km_recv_mac(km, s->is_client), s->seq_no_recv, s->rx_buf, 4 + pkt_len, expected_mac);
504 if (ct_memcmp(expected_mac, s->rx_buf + 4 + pkt_len, mac_tag) != 0)
505 {
506 pc_secure_wipe(expected_mac, sizeof(expected_mac));
507 pc_secure_wipe(s->rx_buf, s->rx_len);
508 s->rx_len = 0;
509 return -1; // caller must close connection
510 }
511 pc_secure_wipe(expected_mac, sizeof(expected_mac));
512
514 {
515 return -1;
516 }
517 s->seq_no_recv++;
518
519 // MAC verified -> decrypt the payload (advances c2s_ctx by exactly pkt_len/16 blocks).
520 const size_t scratch_sz = SSH_PKT_BUF_SIZE;
521 ScratchScope scratch_scope;
522 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
523 if (!scratch)
524 {
525 pc_secure_wipe(s->rx_buf, s->rx_len);
526 s->rx_len = 0;
527 return -1;
528 }
529 memcpy(scratch, s->rx_buf + 4, pkt_len);
530 pc_aes256ctr_crypt(km_recv_aes_key(km, s->is_client), km_recv_aes_iv(km, s->is_client), scratch, scratch, pkt_len);
531
532 // scratch = padding_length || payload || padding.
533 uint8_t pad_len_byte = scratch[0];
534 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
535 {
536 pc_secure_wipe(scratch, scratch_sz);
537 return -1;
538 }
539 size_t payload_len = pkt_len - 1 - pad_len_byte;
540 if (ssh_dispatch_payload(i, scratch + 1, payload_len, handler) < 0)
541 {
542 pc_secure_wipe(scratch, scratch_sz);
543 return -1;
544 }
545
546 size_t consumed = wire_need;
547 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
548 s->rx_len -= consumed;
549 pc_secure_wipe(scratch, scratch_sz);
550 return 1;
551}
552
553static int ssh_recv_ctr_emac(uint8_t i, SshPacketState *s, SshKeyMat *km, ssh_msg_handler_t handler)
554{
555 // aes256-ctr + encrypt-and-MAC. We need the first cipher block (16 bytes) for the length.
556 if (s->rx_len < 16)
557 {
558 return 0; // wait for more data
559 }
560
561 // --- Peek packet_length WITHOUT advancing the cipher ---
562 // Decrypt only the 4-byte length prefix against the current counter block; the counter is not advanced
563 // and no cipher state touches the stack (all working memory stays in the shared crypto scratch).
564 const uint8_t *rk = km_recv_aes_key(km, s->is_client); // recv: client s2c, server c2s
565 uint8_t *rctr = km_recv_aes_iv(km, s->is_client);
566 uint32_t pkt_len = pc_aes256ctr_get_length(rk, rctr, s->rx_buf);
567
568 // Validate length. The encrypted portion (4 + pkt_len) must be a
569 // whole number of AES blocks (RFC 4253 §6 padding guarantees this).
570 size_t enc_len = 4 + pkt_len;
571 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4 || (enc_len % 16) != 0)
572 {
573 pc_secure_wipe(s->rx_buf, s->rx_len);
574 s->rx_len = 0;
575 return -1;
576 }
577
578 size_t mac_tag = ssh_mac_len(km->mac_mode);
579 size_t wire_need = enc_len + mac_tag;
580 if (s->rx_len < wire_need)
581 {
582 return 0; // incomplete packet; cipher state already restored
583 }
584
585 // Borrow this packet's plaintext scratch from the shared arena. The
586 // scope guard reclaims it on every exit path, so multiple packets in
587 // one call reuse the same space instead of accumulating; an exhausted
588 // arena fails closed (discard + disconnect).
589 const size_t scratch_sz = SSH_PKT_BUF_SIZE + 64;
590 ScratchScope scratch_scope;
591 uint8_t *scratch = (uint8_t *)scratch_alloc(scratch_sz, 16);
592 if (!scratch)
593 {
594 pc_secure_wipe(s->rx_buf, s->rx_len);
595 s->rx_len = 0;
596 return -1;
597 }
598
599 // Full packet present. Decrypt EXACTLY the encrypted portion,
600 // which advances the recv counter by exactly enc_len/16 blocks and
601 // leaves it aligned on the next packet boundary.
602 memcpy(scratch, s->rx_buf, enc_len);
603 pc_aes256ctr_crypt(rk, rctr, scratch, scratch, enc_len);
604
605 // Verify MAC over seq_no || plaintext(scratch[0..enc_len)).
606 const uint8_t *rx_mac = s->rx_buf + enc_len; // MAC is sent in clear
607 uint8_t expected_mac[64];
608 compute_mac_mode(km->mac_mode, km_recv_mac(km, s->is_client), s->seq_no_recv, scratch, enc_len, expected_mac);
609
610 if (ct_memcmp(expected_mac, rx_mac, mac_tag) != 0)
611 {
612 // MAC failure: zero everything and disconnect.
613 pc_secure_wipe(scratch, scratch_sz);
614 pc_secure_wipe(expected_mac, sizeof(expected_mac));
615 pc_secure_wipe(s->rx_buf, s->rx_len);
616 s->rx_len = 0;
617 return -1; // caller must close connection
618 }
619 pc_secure_wipe(expected_mac, sizeof(expected_mac));
620
621 // MAC verified. Sequence overflow guard.
623 {
624 pc_secure_wipe(scratch, scratch_sz);
625 return -1;
626 }
627 s->seq_no_recv++;
628
629 // Extract payload: scratch[5 .. 5 + payload_len - 1]
630 uint8_t pad_len_byte = scratch[4];
631 // RFC 4253 6: there MUST be at least 4 bytes of padding, and it cannot
632 // exceed the packet (which would underflow payload_len).
633 if (pad_len_byte < 4 || pad_len_byte >= pkt_len)
634 {
635 pc_secure_wipe(scratch, scratch_sz);
636 return -1;
637 }
638 size_t payload_len = pkt_len - 1 - pad_len_byte;
639 if (ssh_dispatch_payload(i, scratch + 5, payload_len, handler) < 0)
640 {
641 pc_secure_wipe(scratch, scratch_sz);
642 return -1;
643 }
644
645 // Consume from rx_buf.
646 size_t consumed = wire_need;
647 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
648 s->rx_len -= consumed;
649 pc_secure_wipe(scratch, scratch_sz);
650 return 1;
651}
652
653static int ssh_recv_plain(uint8_t i, SshPacketState *s, const SshKeyMat *km, ssh_msg_handler_t handler)
654{
655 (void)km; // no keys before NEWKEYS
656 // Unencrypted path (during initial handshake / before NEWKEYS).
657 uint32_t pkt_len = read_u32_be(s->rx_buf);
658 if (pkt_len < 1 || pkt_len > SSH_PKT_BUF_SIZE - 4)
659 {
660 pc_secure_wipe(s->rx_buf, s->rx_len);
661 s->rx_len = 0;
662 return -1;
663 }
664 size_t wire_need = 4 + pkt_len; // no MAC before NEWKEYS
665 if (s->rx_len < wire_need)
666 {
667 return 0;
668 }
669
671 {
672 return -1;
673 }
674 s->seq_no_recv++;
675
676 uint8_t pad_len_byte = s->rx_buf[4];
677 if (pad_len_byte >= pkt_len)
678 {
679 return -1;
680 }
681 size_t payload_len = pkt_len - 1 - pad_len_byte;
682 if (ssh_dispatch_payload(i, s->rx_buf + 5, payload_len, handler) < 0)
683 {
684 return -1;
685 }
686
687 size_t consumed = wire_need;
688 memmove(s->rx_buf, s->rx_buf + consumed, s->rx_len - consumed);
689 s->rx_len -= consumed;
690 return 1;
691}
692
693int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler)
694{
695 if (i >= MAX_SSH_CONNS)
696 {
697 return -1;
698 }
699 SshPacketState *s = &ssh_pkt[i];
700 SshKeyMat *km = &ssh_keys[i];
701
702 // Consume the input incrementally: append as much as fits the (single-packet) receive buffer, extract
703 // every complete packet to drain it, then append more. So one TCP read carrying several pipelined packets
704 // - e.g. a large SFTP write fragmented into back-to-back CHANNEL_DATA messages - is processed instead of
705 // being rejected when the read exceeds SSH_PKT_BUF_SIZE.
706 while (len > 0)
707 {
708 size_t space = SSH_PKT_BUF_SIZE - s->rx_len;
709 if (space == 0)
710 {
711 // The buffer is full yet no complete packet could be extracted -> a single packet larger than the
712 // buffer. Discard and disconnect.
713 pc_secure_wipe(s->rx_buf, s->rx_len);
714 s->rx_len = 0;
715 return -1;
716 }
717 size_t take = len < space ? len : space;
718 memcpy(s->rx_buf + s->rx_len, data, take);
719 s->rx_len += take;
720 data += take;
721 len -= take;
722
723 // Extract complete packets.
724 while (s->rx_len >= 4)
725 {
726 int r = 0;
728 {
729 r = ssh_recv_chachapoly(i, s, km, handler);
730 }
731 else if (s->enc_in && km->cipher_mode == SSH_CIPHER_AES256GCM)
732 {
733 r = ssh_recv_aesgcm(i, s, km, handler);
734 }
735 else if (s->enc_in && ssh_mac_is_etm(km->mac_mode))
736 {
737 r = ssh_recv_ctr_etm(i, s, km, handler);
738 }
739 else if (s->enc_in)
740 {
741 r = ssh_recv_ctr_emac(i, s, km, handler);
742 }
743 else
744 {
745 r = ssh_recv_plain(i, s, km, handler);
746 }
747
748 if (r < 0)
749 {
750 return -1;
751 }
752 if (r == 0)
753 {
754 break; // incomplete packet - append more input and retry
755 }
756 } // extract-complete-packets loop
757 } // incremental-append loop
758
759 return 0;
760}
761
762// ---------------------------------------------------------------------------
763// Disconnect
764// ---------------------------------------------------------------------------
765
766int ssh_pkt_disconnect(uint8_t i, uint32_t reason_code, uint8_t *out, size_t *out_len, size_t out_cap)
767{
768 if (i >= MAX_SSH_CONNS)
769 {
770 return -1;
771 }
772
773 // Build SSH_MSG_DISCONNECT payload (RFC 4253 §11.1):
774 // byte SSH_MSG_DISCONNECT
775 // uint32 reason code
776 // string description (empty)
777 // string language tag (empty)
778 uint8_t payload[13];
779 payload[0] = SSH_MSG_DISCONNECT;
780 payload[1] = (uint8_t)(reason_code >> 24);
781 payload[2] = (uint8_t)(reason_code >> 16);
782 payload[3] = (uint8_t)(reason_code >> 8);
783 payload[4] = (uint8_t)(reason_code);
784 payload[5] = 0;
785 payload[6] = 0;
786 payload[7] = 0;
787 payload[8] = 0; // empty description
788 payload[9] = 0;
789 payload[10] = 0;
790 payload[11] = 0;
791 payload[12] = 0; // empty language
792
793 int rc = ssh_pkt_send(i, payload, sizeof(payload), out, out_len, out_cap);
794
795 // Zero packet state and key material regardless of send success.
796 ssh_pkt_init(i);
797 ssh_keymat_wipe(i);
798 ssh_dh_wipe(i);
799
800 return rc;
801}
void pc_aes256ctr_crypt(const uint8_t key[PC_AES256CTR_KEY_LEN], uint8_t counter[PC_AES256CTR_CTR_LEN], const uint8_t *in, uint8_t *out, size_t len)
Encrypt or decrypt len bytes with AES-256-CTR (the two are identical).
Definition aes256ctr.cpp:45
uint32_t pc_aes256ctr_get_length(const uint8_t key[PC_AES256CTR_KEY_LEN], const uint8_t counter[PC_AES256CTR_CTR_LEN], const uint8_t enc4[4])
Decrypt only the 4-byte SSH packet_length prefix WITHOUT advancing counter.
PC_CRYPTO_HOT void pc_aesgcm_iv_increment(uint8_t iv[PC_AESGCM_IV_LEN])
Advance the RFC 5647 invocation counter: the low 8 bytes of the 12-byte nonce as a big-endian integer...
Definition aesgcm.cpp:30
AES-256-GCM AEAD (RFC 5116) - stateless, detached-tag API.
#define PC_AESGCM_TAG_LEN
GCM authentication tag length (bytes).
Definition aesgcm.h:42
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
bool pc_chachapoly_decrypt(const uint8_t key[PC_CHACHAPOLY_KEY_LEN], uint32_t seqnr, uint8_t *dest, const uint8_t *src, uint32_t payload_len)
Verify+decrypt one packet.
uint32_t pc_chachapoly_get_length(const uint8_t key[PC_CHACHAPOLY_KEY_LEN], uint32_t seqnr, const uint8_t enc_len[PC_CHACHAPOLY_AAD_LEN])
Decrypt just the 4-byte length field to learn the packet length before reading the body.
void pc_chachapoly_encrypt(const uint8_t key[PC_CHACHAPOLY_KEY_LEN], uint32_t seqnr, uint8_t *dest, const uint8_t *src, uint32_t payload_len)
Encrypt+authenticate one packet.
chacha20-poly1305@openssh.com AEAD cipher (OpenSSH PROTOCOL.chacha20poly1305).
#define PC_CHACHAPOLY_TAG_LEN
Poly1305 tag.
Definition chachapoly.h:31
RAII scope guard for transient scratch borrows.
Definition scratch.h:196
void pc_hmac_sha256_update(pc_hmac_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes into the running HMAC.
void pc_hmac_sha256_init(pc_hmac_sha256_ctx *ctx, const uint8_t *key, size_t key_len)
Initialize a streaming HMAC-SHA2-256 context.
void pc_hmac_sha256_final(pc_hmac_sha256_ctx *ctx, uint8_t mac[PC_HMAC_SHA256_LEN])
Finalize and write the 32-byte MAC.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
void pc_hmac_sha512_final(pc_hmac_sha512_ctx *ctx, uint8_t mac[PC_HMAC_SHA512_LEN])
Finish, writing the 64-byte MAC.
void pc_hmac_sha512_init(pc_hmac_sha512_ctx *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 pc_hmac_sha512_update(pc_hmac_sha512_ctx *ctx, const uint8_t *data, size_t len)
Feed len message bytes.
HMAC-SHA2-512 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
bool pc_aesgcm_open(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESGCM_TAG_LEN], uint8_t *out)
Open one record: verify tag over aad || ct in constant time, then (only on success) decrypt ct into o...
pc_cspan pc_aesgcm_seal(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out, uint8_t tag_out[PC_AESGCM_TAG_LEN])
Seal one record under k and nonce.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:135
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
Secure pool accessor - borrows that hold key material.
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
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:121
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
Definition ssh_keymat.h:122
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
Definition ssh_keymat.h:120
@ SSH_MAC_HMAC_SHA512
hmac-sha2-512 (encrypt-and-MAC)
Definition ssh_keymat.h:129
@ SSH_MAC_HMAC_SHA512_ETM
hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
Definition ssh_keymat.h:131
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.
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:244
SSH server-to-client compression: a context-takeover DEFLATE stream (no heap).
AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
Definition ssh_keymat.h:173
uint8_t aes_iv_c2s[PC_AES256CTR_CTR_LEN]
AES IV C→S (CTR counter / GCM nonce); advances per packet.
Definition ssh_keymat.h:182
uint8_t chacha_key_s2c[PC_CHACHAPOLY_KEY_LEN]
server-to-client, used only in chacha mode.
Definition ssh_keymat.h:192
uint8_t aes_iv_s2c[PC_AES256CTR_CTR_LEN]
AES IV S→C (CTR counter / GCM nonce); advances per packet.
Definition ssh_keymat.h:183
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:185
uint8_t gcm_ctx_s2c[PC_WORK_AESGCM]
keyed GCM context S→C (server seals outbound).
Definition ssh_keymat.h:203
uint8_t aes_key_c2s[PC_AES256CTR_KEY_LEN]
AES key C→S (server decrypts inbound).
Definition ssh_keymat.h:180
uint8_t cipher_mode
SSH_CIPHER_* selected for this session (0 = aes256-ctr).
Definition ssh_keymat.h:189
uint8_t mac_key_s2c[64]
HMAC key, server-to-client (aes mode).
Definition ssh_keymat.h:186
uint8_t chacha_key_c2s[PC_CHACHAPOLY_KEY_LEN]
client-to-server, used only in chacha mode.
Definition ssh_keymat.h:191
uint8_t mac_mode
SSH_MAC_* selected for the aes256-ctr cipher (0 = hmac-sha2-256 E&M).
Definition ssh_keymat.h:187
uint8_t aes_key_s2c[PC_AES256CTR_KEY_LEN]
AES key S→C (server encrypts outbound).
Definition ssh_keymat.h:181
uint8_t gcm_ctx_c2s[PC_WORK_AESGCM]
keyed GCM context C→S (server opens inbound).
Definition ssh_keymat.h:202
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
Streaming HMAC-SHA2-256 context.
Definition hmac_sha256.h:57
Streaming HMAC-SHA2-512 context (stores the opad key block + inner hash state).
Definition hmac_sha512.h:29