ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_transport.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_transport.cpp
6 * @brief SSH transport handshake - banner exchange and KEXINIT negotiation.
7 */
8
10#include "crypto/asymmetric/bignum.h" // bn_*, pc_bignum
11#include "crypto/asymmetric/curve25519.h" // pc_x25519 (curve25519-sha256 KEX)
12#include "crypto/asymmetric/ecdsa.h" // pc_ecdsa_p256_* (ecdsa-sha2-nistp256 host key)
13#include "crypto/asymmetric/ed25519.h" // pc_ed25519 host-key sign
14#include "crypto/hash/sha256.h"
15#include "network_drivers/presentation/ssh/crypto/ssh_rsa.h" // ssh_rsa_encode_pubkey/sign, ssh_host_pubkey, SSH_RSA_*
16#include "network_drivers/presentation/ssh/transport/ssh_dh.h" // ssh_rng_fill(), ssh_dh[], ssh_dh_generate/derive_keys
17#include "network_drivers/presentation/ssh/transport/ssh_packet.h" // SSH_MSG_KEXINIT, ssh_pkt[]
18#include "server/mmgr/secure.h"
19#include "services/system/clock.h" // pc_millis() (re-key timer)
20#include "shared_primitives/strbuf.h" // pc_sb frame builder
21#if PC_ENABLE_PQC_KEX
22#include "crypto/pqc/mlkem.h" // pc_mlkem768_encaps (PQ/T hybrid KEX responder)
23#endif
24#if PC_ENABLE_SSH_SNTRUP761
25#include "crypto/pqc/sntrup761.h" // pc_sntrup761_enc (sntrup761x25519 responder)
26#endif
27#if PC_ENABLE_SSH_ZLIB
28#include "network_drivers/presentation/ssh/transport/ssh_comp.h" // s2c compression negotiation
29#endif
30#include <stdio.h> // snprintf (name-list assembly)
31#include <string.h>
32
33#ifdef PC_SSH_KEX_BENCH
34#include <esp_timer.h> // esp_timer_get_time() - microsecond wall clock for the KEX span probe
35// The one owned KEX-bench context (see ssh_transport.h). Set by ssh_kex_generate / ssh_kexdh_handle; the
36// rig firmware prints it. Single active connection during a bench run, so a plain instance suffices.
37SshKexBenchCtx pc_ssh_kex_bench = {0, 0, 0};
38#endif
39
41
42// ---------------------------------------------------------------------------
43// Negotiable algorithms. The server supports two KEX methods and two host-key
44// types; cipher / MAC / compression are fixed. Negotiation is crypto-agnostic and
45// steers toward whichever suite ssh_kex_set_prefer_rsa() selects (default: RSA/DH,
46// hardware-accelerated on ESP32), while still advertising both suites so a client
47// that supports only one still connects.
48// ---------------------------------------------------------------------------
49
50static const char *const KEX_DH = "diffie-hellman-group14-sha256";
51static const char *const KEX_C25519 = "curve25519-sha256";
52static const char *const KEX_C25519_LIBSSH = "curve25519-sha256@libssh.org"; // identical wire protocol
53static const char *const KEX_ECDH_NISTP256 = "ecdh-sha2-nistp256"; // NIST P-256 ECDH (RFC 5656 §4)
54#if PC_ENABLE_PQC_KEX
55static const char *const KEX_MLKEM768 = "mlkem768x25519-sha256"; // PQ/T hybrid (ML-KEM-768 + X25519)
56#endif
57#if PC_ENABLE_SSH_SNTRUP761
58static const char *const KEX_SNTRUP761 = "sntrup761x25519-sha512@openssh.com"; // PQ/T hybrid (NTRU Prime + X25519)
59#endif
60static const char *const HOSTKEY_RSA_SHA256 = "rsa-sha2-256";
61static const char *const HOSTKEY_RSA_SHA512 = "rsa-sha2-512";
62static const char HOSTKEY_ED[] = "ssh-ed25519";
63static const char HOSTKEY_ECDSA[] = "ecdsa-sha2-nistp256";
64static const char *const ALG_CIPHER = "aes256-ctr";
65static const char *const ALG_CIPHER_GCM = "aes256-gcm@openssh.com";
66// Advertised cipher preference (OpenSSH's default order): chacha20-poly1305@openssh.com (AEAD)
67// first, aes256-gcm@openssh.com (AEAD, HW-accelerated) second, aes256-ctr (HW fallback) last.
68static const char *const ALG_CIPHER_LIST = "chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr";
69static const char *const ALG_MAC = "hmac-sha2-256";
70// Advertised MAC preference (aes256-ctr only; the chacha AEAD needs none): encrypt-then-MAC first
71// (OpenSSH's default), then plain encrypt-and-MAC.
72static const char *const ALG_MAC_LIST = "hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,"
73 "hmac-sha2-256,hmac-sha2-512";
74static const char *const ALG_COMP = "none";
75#if PC_ENABLE_SSH_ZLIB
76// Compression preference (both directions): zlib@openssh.com (delayed, OpenSSH's default) first, then
77// zlib (immediate), then none. s2c deflates (ssh_zlib); c2s inflates OpenSSH's Z_PARTIAL_FLUSH stream
78// (ssh_inflate). Advertised for c2s and s2c alike.
79static const char *const ALG_COMP_ZLIB = "zlib@openssh.com,zlib,none";
80#else
81static const char *const ALG_COMP_ZLIB = "none";
82#endif
83// RFC 8308 indicator a client sets in its kex_algorithms to request EXT_INFO.
84static const char *const EXT_INFO_C = "ext-info-c";
85
86// All SSH transport host-key/KEX state, owned by one instance (internal linkage): the runtime
87// KEX preference (true = prefer the hardware-accelerated RSA/DH suite) and the optional
88// ssh-ed25519 host key (the RSA host key is loaded via ssh_rsa). One named owner, cross-TU
89// unreachable.
91{
92 bool prefer_rsa = true;
93 uint8_t ed_seed[32];
94 uint8_t ed_pub[32];
95 bool ed_have = false;
96 uint8_t ecdsa_priv[PC_ECDSA_P256_PRIV_LEN]; ///< P-256 host private scalar d.
97 uint8_t ecdsa_pub[PC_ECDSA_P256_PUB_LEN]; ///< P-256 host public point (0x04||X||Y).
98 bool ecdsa_have = false;
99};
100static SshTransportCtx s_sshtr;
101
102void ssh_kex_set_prefer_rsa(bool prefer)
103{
104 s_sshtr.prefer_rsa = prefer;
105}
107{
108 return s_sshtr.prefer_rsa;
109}
110
111void pc_ssh_hostkey_ed25519_set(const uint8_t seed[32])
112{
113 memcpy(s_sshtr.ed_seed, seed, 32);
114 pc_ed25519_pubkey(s_sshtr.ed_pub, s_sshtr.ed_seed);
115 s_sshtr.ed_have = true;
116}
118{
119 return s_sshtr.ed_have;
120}
122{
123 // Derive and cache the public point; reject an invalid scalar (leaves ecdsa_have false).
124 if (!pc_ecdsa_p256_pubkey(s_sshtr.ecdsa_pub, priv))
125 {
126 return;
127 }
128 memcpy(s_sshtr.ecdsa_priv, priv, PC_ECDSA_P256_PRIV_LEN);
129 s_sshtr.ecdsa_have = true;
130}
132{
133 return s_sshtr.ecdsa_have;
134}
135static bool hostkey_rsa_available(void)
136{
137 return ssh_host_pubkey.loaded;
138}
139
140// Build the KEX / host-key advertise lists in preference order (RFC 4253 §7.1 name-
141// lists), filtering host-key types to the keys we actually hold. server-sig-algs
142// (RFC 8308) uses the same host-key ordering. Written into a caller buffer.
143static void build_kex_list(char *out, size_t cap)
144{
145 const char *c1 = KEX_C25519;
146 const char *c2 = KEX_C25519_LIBSSH;
147 const char *dh = KEX_DH;
148 const char *ec = KEX_ECDH_NISTP256; // NIST P-256 ECDH (RFC 5656)
149 size_t n = 0;
150 // Post-quantum hybrids advertised first: a PQC-capable peer (OpenSSH 9.9+, which also lists them
151 // first) negotiates one over classical X25519, closing the harvest-now-decrypt-later gap. Each
152 // is gated independently so a footprint-bound build can offer ML-KEM only (kexlist[192] holds
153 // the full both-hybrid list with margin, so the appends never truncate).
154#if PC_ENABLE_PQC_KEX
155 pc_sb sb_mlkem = {out + n, cap - n, 0, true};
156 pc_sb_put(&sb_mlkem, KEX_MLKEM768);
157 pc_sb_lit(&sb_mlkem, ",");
158 n += pc_sb_finish(&sb_mlkem);
159#endif
160#if PC_ENABLE_SSH_SNTRUP761
161 pc_sb sb_sntrup = {out + n, cap - n, 0, true};
162 pc_sb_put(&sb_sntrup, KEX_SNTRUP761);
163 pc_sb_lit(&sb_sntrup, ",");
164 n += pc_sb_finish(&sb_sntrup);
165#endif
166 if (s_sshtr.prefer_rsa)
167 {
168 pc_sb sb157 = {out + n, cap - n, 0, true};
169 pc_sb_put(&sb157, dh);
170 pc_sb_put(&sb157, ",");
171 pc_sb_put(&sb157, ec);
172 pc_sb_put(&sb157, ",");
173 pc_sb_put(&sb157, c1);
174 pc_sb_put(&sb157, ",");
175 pc_sb_put(&sb157, c2);
176 pc_sb_put(&sb157, ",ext-info-s");
177 if (pc_sb_finish(&sb157) == 0)
178 {
179 sb157.p[0] = '\0';
180 }
181 }
182 else
183 {
184 pc_sb sb159 = {out + n, cap - n, 0, true};
185 pc_sb_put(&sb159, c1);
186 pc_sb_put(&sb159, ",");
187 pc_sb_put(&sb159, c2);
188 pc_sb_put(&sb159, ",");
189 pc_sb_put(&sb159, ec);
190 pc_sb_put(&sb159, ",");
191 pc_sb_put(&sb159, dh);
192 pc_sb_put(&sb159, ",ext-info-s");
193 if (pc_sb_finish(&sb159) == 0)
194 {
195 sb159.p[0] = '\0';
196 }
197 }
198}
199static void build_hostkey_list(char *out, size_t cap)
200{
201 // Both rsa-sha2-512 and rsa-sha2-256 are backed by the one "ssh-rsa" host key
202 // (RFC 8332): advertise 512 before 256 (OpenSSH's order). Filter to keys we hold.
203 const bool rsa = hostkey_rsa_available();
204 const bool ed = pc_ssh_hostkey_ed25519_available();
205 const bool ec = pc_ssh_hostkey_ecdsa_available();
206 struct HostkeyCand
207 {
208 const char *name;
209 bool ok;
210 };
211 HostkeyCand cand[4];
212 if (s_sshtr.prefer_rsa)
213 {
214 cand[0] = {HOSTKEY_RSA_SHA512, rsa};
215 cand[1] = {HOSTKEY_RSA_SHA256, rsa};
216 cand[2] = {HOSTKEY_ECDSA, ec};
217 cand[3] = {HOSTKEY_ED, ed};
218 }
219 else
220 {
221 cand[0] = {HOSTKEY_ED, ed};
222 cand[1] = {HOSTKEY_ECDSA, ec};
223 cand[2] = {HOSTKEY_RSA_SHA512, rsa};
224 cand[3] = {HOSTKEY_RSA_SHA256, rsa};
225 }
226 out[0] = '\0';
227 for (int k = 0; k < 4; k++)
228 {
229 if (!cand[k].ok)
230 {
231 continue;
232 }
233 size_t l = strnlen(out, cap);
234 pc_sb sb194 = {out + l, cap - l, 0, true};
235 pc_sb_put(&sb194, l ? "," : "");
236 pc_sb_put(&sb194, cand[k].name);
237 if (pc_sb_finish(&sb194) == 0)
238 {
239 sb194.p[0] = '\0';
240 }
241 }
242}
243
244// ---------------------------------------------------------------------------
245// Byte-writer helpers
246// ---------------------------------------------------------------------------
247
248namespace
249{
250struct Writer
251{
252 uint8_t *p;
253 size_t cap;
254 size_t len;
255 bool ok;
256};
257
258void w_bytes(Writer &w, const void *src, size_t n)
259{
260 if (!w.ok || w.len + n > w.cap)
261 {
262 w.ok = false;
263 return;
264 }
265 memcpy(w.p + w.len, src, n); // NOSONAR - bound proven above; analyzer follows an infeasible path
266 w.len += n;
267}
268
269void w_u8(Writer &w, uint8_t v)
270{
271 w_bytes(w, &v, 1);
272}
273
274void w_u32(Writer &w, uint32_t v)
275{
276 uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
277 w_bytes(w, b, 4);
278}
279
280// Write an SSH name-list: uint32 length + comma-separated names.
281void w_namelist(Writer &w, const char *list)
282{
283 uint32_t n = (uint32_t)strnlen(list, w.cap);
284 w_u32(w, n);
285 w_bytes(w, list, n);
286}
287
288// Write an SSH string: uint32 length + raw bytes.
289void w_string(Writer &w, const uint8_t *data, size_t n)
290{
291 w_u32(w, (uint32_t)n);
292 w_bytes(w, data, n);
293}
294
295// Write an SSH mpint from a fixed-width big-endian integer: strip leading zero
296// bytes, prepend 0x00 if the top bit is set.
297void w_mpint(Writer &w, const uint8_t *be, size_t len)
298{
299 size_t off = 0;
300 while (off < len && be[off] == 0)
301 {
302 off++;
303 }
304 if (off == len)
305 {
306 w_u32(w, 0); // zero → empty string
307 return;
308 }
309 bool pad = (be[off] & 0x80u) != 0;
310 w_u32(w, (uint32_t)(len - off) + (pad ? 1u : 0u));
311 if (pad)
312 {
313 w_u8(w, 0x00);
314 }
315 w_bytes(w, be + off, len - off);
316}
317} // namespace
318
319// ---------------------------------------------------------------------------
320// name-list membership test (RFC 4253 §7.1 - comma-separated, no spaces)
321// ---------------------------------------------------------------------------
322
323// Returns true if @p want appears as a complete element of the comma-separated
324// list [list, list+len).
325static bool namelist_contains(const uint8_t *list, uint32_t len, const char *want)
326{
327 size_t wl = strnlen(want, (size_t)len + 1);
328 uint32_t start = 0;
329 for (uint32_t i = 0; i <= len; i++)
330 {
331 if (i == len || list[i] == ',')
332 {
333 uint32_t elen = i - start;
334 if (elen == wl && memcmp(list + start, want, wl) == 0)
335 {
336 return true;
337 }
338 start = i + 1;
339 }
340 }
341 return false;
342}
343
344// One negotiation candidate: an algorithm name, the tag we store if it is chosen, and
345// whether we can actually perform it (e.g. we hold the matching host key).
346// A negotiation candidate: the wire name, the enum value it maps to, and whether we can perform it.
347// Templated on the algorithm enum so each family (SshKexAlg / SshHostkeyAlg / cipher / mac / SshCompAlg)
348// keeps its own type end to end - no type-erased int tag to cast into and back out of.
349template <typename E> struct AlgCand
350{
351 const char *name;
353 bool avail;
354};
355
356// Index of the first available candidate whose name is exactly [tok, tok+tlen), or -1.
357template <typename E> static int cand_match(const uint8_t *tok, uint32_t tlen, const AlgCand<E> *cands, int n)
358{
359 for (int c = 0; c < n; c++)
360 {
361 size_t cl = strnlen(cands[c].name, (size_t)tlen + 1);
362 if (cands[c].avail && cl == tlen && memcmp(tok, cands[c].name, tlen) == 0)
363 {
364 return c;
365 }
366 }
367 return -1;
368}
369
370// RFC 4253 §7.1: the chosen algorithm is the first name on the CLIENT's name-list that the server also
371// supports - CLIENT preference, not ours. Two peers whose preference orders differ must still converge, so
372// the rule is fixed to the client's order. Iterate the client's comma-separated list in order; for each
373// name take the first available server candidate that matches. (Steering to our preferred algorithm is
374// done by the order WE advertise in KEXINIT, which a client that has no strong preference will follow.)
375template <typename E>
376static bool negotiate_alg(const uint8_t *client_list, uint32_t nlen, const AlgCand<E> *cands, int n, E *out)
377{
378 uint32_t start = 0;
379 for (uint32_t i = 0; i <= nlen; i++)
380 {
381 if (i == nlen || client_list[i] == ',')
382 {
383 int c = cand_match(client_list + start, i - start, cands, n);
384 if (c >= 0)
385 {
386 *out = cands[c].tag;
387 return true;
388 }
389 start = i + 1;
390 }
391 }
392 return false;
393}
394
395// ---------------------------------------------------------------------------
396// Init
397// ---------------------------------------------------------------------------
398
399void ssh_transport_init(uint8_t i)
400{
401 if (i >= MAX_SSH_CONNS)
402 {
403 return;
404 }
405 SshSession *s = &ssh_sess[i];
406 memset(s, 0, sizeof(*s));
408}
409
410// ---------------------------------------------------------------------------
411// Identification string exchange (RFC 4253 §4.2)
412// ---------------------------------------------------------------------------
413
414int ssh_transport_server_banner(uint8_t *out, size_t *out_len, size_t cap)
415{
416 size_t vlen = sizeof(SSH_SERVER_VERSION) - 1;
417 if (vlen + 2 > cap)
418 {
419 return -1;
420 }
421 memcpy(out, SSH_SERVER_VERSION, vlen);
422 out[vlen] = '\r';
423 out[vlen + 1] = '\n';
424 *out_len = vlen + 2;
425 return 0;
426}
427
428int ssh_transport_recv_banner(uint8_t i, const uint8_t *data, size_t len, size_t *consumed)
429{
430 if (i >= MAX_SSH_CONNS)
431 {
432 return -1;
433 }
434 SshSession *s = &ssh_sess[i];
435
436 size_t k = 0;
437 while (k < len)
438 {
439 uint8_t c = data[k++];
440 if (c == '\n')
441 {
442 // End of a line. Strip a trailing CR if present.
443 uint16_t n = s->banner_len;
444 if (n > 0 && s->banner_buf[n - 1] == '\r')
445 {
446 n--;
447 }
448
449 // RFC 4253 §4.2: the server may receive other lines before the
450 // identification string; only the line starting with "SSH-" counts.
451 if (n >= 4 && memcmp(s->banner_buf, "SSH-", 4) == 0)
452 {
453 if (n >= SSH_VERSION_MAX)
454 {
455 return -1;
456 }
457 memcpy(s->v_c, s->banner_buf, n);
458 s->v_c[n] = '\0';
459 s->v_c_len = n;
460 s->banner_len = 0;
462 *consumed = k;
463 return 1;
464 }
465 // Not the SSH line - discard and keep scanning.
466 s->banner_len = 0;
467 continue;
468 }
469
470 if (s->banner_len >= SSH_VERSION_MAX)
471 {
472 return -1; // line too long
473 }
474 s->banner_buf[s->banner_len++] = c;
475 }
476
477 *consumed = k;
478 return 0; // need more data
479}
480
481// ---------------------------------------------------------------------------
482// KEXINIT (RFC 4253 §7.1)
483// ---------------------------------------------------------------------------
484
485int ssh_kexinit_build(uint8_t i, uint8_t *payload, size_t *len, size_t cap)
486{
487 if (i >= MAX_SSH_CONNS)
488 {
489 return -1;
490 }
491 SshSession *s = &ssh_sess[i];
492
493 Writer w = {payload, cap, 0, true};
494 w_u8(w, SSH_MSG_KEXINIT);
495
496 uint8_t cookie[16];
497 ssh_rng_fill(cookie, sizeof(cookie));
498 w_bytes(w, cookie, sizeof(cookie));
499
500 char kexlist[192];
501 // All four host-key algs = "ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256" is 57
502 // chars + NUL; a smaller buffer silently drops rsa-sha2-256 when all three key types are loaded.
503 char hklist[64];
504 build_kex_list(kexlist, sizeof(kexlist));
505 build_hostkey_list(hklist, sizeof(hklist));
506 w_namelist(w, kexlist); // kex_algorithms (preference-ordered, + ext-info-s)
507 w_namelist(w, hklist); // server_host_key_algorithms (only keys we hold)
508 w_namelist(w, ALG_CIPHER_LIST); // encryption c2s (chacha20-poly1305 preferred, aes256-ctr fallback)
509 w_namelist(w, ALG_CIPHER_LIST); // encryption s2c
510 w_namelist(w, ALG_MAC_LIST); // mac c2s (used only with aes256-ctr; ignored for the AEAD cipher)
511 w_namelist(w, ALG_MAC_LIST); // mac s2c
512 w_namelist(w, ALG_COMP_ZLIB); // compression c2s (zlib@openssh.com / zlib when built in, else none)
513 w_namelist(w, ALG_COMP_ZLIB); // compression s2c (zlib@openssh.com / zlib when built in, else none)
514 w_namelist(w, ""); // languages c2s
515 w_namelist(w, ""); // languages s2c
516 w_u8(w, 0); // first_kex_packet_follows = false
517 w_u32(w, 0); // reserved
518
519 if (!w.ok)
520 {
521 return -1;
522 }
523
524 // Retain a copy as I_S for the exchange hash.
525 if (w.len >
526 PC_SSH_KEXINIT_S_MAX) // GCOVR_EXCL_LINE the server's fixed algorithm lists never exceed PC_SSH_KEXINIT_S_MAX
527 {
528 return -1; // GCOVR_EXCL_LINE
529 }
530 memcpy(s->i_s, payload, w.len);
531 s->i_s_len = (uint16_t)w.len;
532
533 *len = w.len;
534 return 0;
535}
536
537// Read a name-list field at offset *off; set *list/*nlen to point into payload.
538// Returns true on success and advances *off past the field.
539static bool read_namelist(const uint8_t *p, size_t len, size_t *off, const uint8_t **list, uint32_t *nlen)
540{
541 if (*off + 4 > len)
542 {
543 return false;
544 }
545 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
546 (uint32_t)p[*off + 3];
547 *off += 4;
548 if (*off + n > len)
549 {
550 return false;
551 }
552 *list = p + *off;
553 *nlen = n;
554 *off += n;
555 return true;
556}
557
558// Negotiate the key-exchange method from the client's kex_algorithms name-list, in our preference order
559// (PQC hybrid first when enabled; RSA group first when prefer_rsa). false = no mutual method.
560static bool negotiate_kex(const uint8_t *list, uint32_t nlen, SshKexAlg *out)
561{
562 AlgCand<SshKexAlg> kc[6];
563 int nk = 0;
564#if PC_ENABLE_PQC_KEX
565 kc[nk++] = {KEX_MLKEM768, SshKexAlg::SSH_KEX_MLKEM768_X25519, true}; // hybrid first (PQC-preferred)
566#endif
567#if PC_ENABLE_SSH_SNTRUP761
568 kc[nk++] = {KEX_SNTRUP761, SshKexAlg::SSH_KEX_SNTRUP761_X25519, true}; // OpenSSH's other PQC default
569#endif
570 if (s_sshtr.prefer_rsa)
571 {
572 kc[nk++] = {KEX_DH, SshKexAlg::SSH_KEX_DH_GROUP14, true};
573 kc[nk++] = {KEX_ECDH_NISTP256, SshKexAlg::SSH_KEX_ECDH_NISTP256, true};
574 kc[nk++] = {KEX_C25519, SshKexAlg::SSH_KEX_CURVE25519, true};
575 kc[nk++] = {KEX_C25519_LIBSSH, SshKexAlg::SSH_KEX_CURVE25519, true};
576 }
577 else
578 {
579 kc[nk++] = {KEX_C25519, SshKexAlg::SSH_KEX_CURVE25519, true};
580 kc[nk++] = {KEX_C25519_LIBSSH, SshKexAlg::SSH_KEX_CURVE25519, true};
581 kc[nk++] = {KEX_ECDH_NISTP256, SshKexAlg::SSH_KEX_ECDH_NISTP256, true};
582 kc[nk++] = {KEX_DH, SshKexAlg::SSH_KEX_DH_GROUP14, true};
583 }
584 return negotiate_alg(list, nlen, kc, nk, out);
585}
586
587// Negotiate the host-key algorithm, restricted to keys we actually hold. rsa-sha2-512/256 share the one
588// RSA key (RFC 8332), ecdsa-sha2-nistp256 is a distinct P-256 key. false = no mutual algorithm.
589static bool negotiate_hostkey(const uint8_t *list, uint32_t nlen, SshHostkeyAlg *out)
590{
591 const bool rsa = hostkey_rsa_available();
592 const bool ed = pc_ssh_hostkey_ed25519_available();
593 const bool ec = pc_ssh_hostkey_ecdsa_available();
595 if (s_sshtr.prefer_rsa)
596 {
597 hc[0] = {HOSTKEY_RSA_SHA512, SshHostkeyAlg::SSH_HOSTKEY_RSA_SHA512, rsa};
598 hc[1] = {HOSTKEY_RSA_SHA256, SshHostkeyAlg::SSH_HOSTKEY_RSA_SHA256, rsa};
599 hc[2] = {HOSTKEY_ECDSA, SshHostkeyAlg::SSH_HOSTKEY_ECDSA_NISTP256, ec};
600 hc[3] = {HOSTKEY_ED, SshHostkeyAlg::SSH_HOSTKEY_ED25519, ed};
601 }
602 else
603 {
604 hc[0] = {HOSTKEY_ED, SshHostkeyAlg::SSH_HOSTKEY_ED25519, ed};
605 hc[1] = {HOSTKEY_ECDSA, SshHostkeyAlg::SSH_HOSTKEY_ECDSA_NISTP256, ec};
606 hc[2] = {HOSTKEY_RSA_SHA512, SshHostkeyAlg::SSH_HOSTKEY_RSA_SHA512, rsa};
607 hc[3] = {HOSTKEY_RSA_SHA256, SshHostkeyAlg::SSH_HOSTKEY_RSA_SHA256, rsa};
608 }
609 return negotiate_alg(list, nlen, hc, 4, out);
610}
611
612int ssh_kexinit_parse(uint8_t i, const uint8_t *payload, size_t len)
613{
614 if (i >= MAX_SSH_CONNS)
615 {
616 return -1;
617 }
618 SshSession *s = &ssh_sess[i];
619
620 if (len < 1 + 16 || payload[0] != SSH_MSG_KEXINIT)
621 {
622 return -1;
623 }
624
625 // Retain a copy as I_C for the exchange hash.
626 if (len > SSH_KEXINIT_MAX)
627 {
628 return -1;
629 }
630 memcpy(s->i_c, payload, len);
631 s->i_c_len = (uint16_t)len;
632
633 size_t off = 1 + 16; // skip msg type + 16-byte cookie
634
635 const uint8_t *list;
636 uint32_t nlen;
637
638 // kex_algorithms: negotiate the KEX method by the CLIENT's preference (RFC 4253 §7.1).
639 if (!read_namelist(payload, len, &off, &list, &nlen))
640 {
641 return -1;
642 }
643 // RFC 8308: if the client offers ext-info-c we will send SSH_MSG_EXT_INFO.
644 s->ext_info_c = namelist_contains(list, nlen, EXT_INFO_C);
645 if (!negotiate_kex(list, nlen, &s->kex_alg))
646 {
647 return -1; // no mutual KEX
648 }
649 // server_host_key_algorithms: negotiate, restricted to keys we actually hold.
650 if (!read_namelist(payload, len, &off, &list, &nlen))
651 {
652 return -1;
653 }
654 if (!negotiate_hostkey(list, nlen, &s->hostkey_alg))
655 {
656 return -1; // no mutual host-key algorithm
657 }
658 // encryption c2s / s2c: negotiate chacha20-poly1305@openssh.com or aes256-gcm@openssh.com (both
659 // AEADs) or aes256-ctr, in that preference order.
660 const AlgCand<decltype(SSH_CIPHER_AES256CTR)> cc[3] = {
661 {"chacha20-poly1305@openssh.com", SSH_CIPHER_CHACHA20POLY1305, true},
662 {ALG_CIPHER_GCM, SSH_CIPHER_AES256GCM, true},
663 {ALG_CIPHER, SSH_CIPHER_AES256CTR, true}};
664 if (!read_namelist(payload, len, &off, &list, &nlen))
665 {
666 return -1;
667 }
668 decltype(SSH_CIPHER_AES256CTR) c2s;
669 decltype(SSH_CIPHER_AES256CTR) s2c;
670 if (!negotiate_alg(list, nlen, cc, 3, &c2s))
671 {
672 return -1;
673 }
674 if (!read_namelist(payload, len, &off, &list, &nlen))
675 {
676 return -1;
677 }
678 if (!negotiate_alg(list, nlen, cc, 3, &s2c) || s2c != c2s) // require the same cipher both directions
679 {
680 return -1;
681 }
682 s->cipher_alg = c2s;
683 // mac c2s / s2c: negotiated only for aes256-ctr (both AEAD ciphers carry their own MAC). Prefer
684 // the encrypt-then-MAC variants (OpenSSH's default), require the same MAC both directions.
685 const AlgCand<decltype(SSH_MAC_HMAC_SHA256)> mc[4] = {
686 {"hmac-sha2-256-etm@openssh.com", SSH_MAC_HMAC_SHA256_ETM, true},
687 {"hmac-sha2-512-etm@openssh.com", SSH_MAC_HMAC_SHA512_ETM, true},
688 {ALG_MAC, SSH_MAC_HMAC_SHA256, true},
689 {"hmac-sha2-512", SSH_MAC_HMAC_SHA512, true}};
690 bool need_mac = (s->cipher_alg == SSH_CIPHER_AES256CTR);
693 if (!read_namelist(payload, len, &off, &list, &nlen))
694 {
695 return -1;
696 }
697 if (need_mac && !negotiate_alg(list, nlen, mc, 4, &m_c2s))
698 {
699 return -1;
700 }
701 if (!read_namelist(payload, len, &off, &list, &nlen))
702 {
703 return -1;
704 }
705 if (need_mac && (!negotiate_alg(list, nlen, mc, 4, &m_s2c) || m_s2c != m_c2s))
706 {
707 return -1;
708 }
709 s->mac_alg = m_c2s;
710 // compression c2s + s2c: negotiate zlib@openssh.com > zlib > none per direction. c2s: the client
711 // compresses, we inflate (ssh_inflate); s2c: we compress, the client inflates (ssh_zlib).
712#if PC_ENABLE_SSH_ZLIB
713 {
714 const AlgCand<SshCompAlg> compc[3] = {{"zlib@openssh.com", SshCompAlg::SSH_COMP_ZLIB_DELAYED, true},
715 {"zlib", SshCompAlg::SSH_COMP_ZLIB, true},
716 {"none", SshCompAlg::SSH_COMP_NONE, true}};
717 SshCompAlg comp;
718 if (!read_namelist(payload, len, &off, &list, &nlen) || !negotiate_alg(list, nlen, compc, 3, &comp))
719 {
720 return -1;
721 }
722 ssh_comp_set_c2s(i, comp);
723 if (!read_namelist(payload, len, &off, &list, &nlen) || !negotiate_alg(list, nlen, compc, 3, &comp))
724 {
725 return -1;
726 }
727 ssh_comp_set_s2c(i, comp);
728 }
729#else
730 // Both directions must offer "none" (no compression built in).
731 if (!read_namelist(payload, len, &off, &list, &nlen) || !namelist_contains(list, nlen, ALG_COMP))
732 {
733 return -1;
734 }
735 if (!read_namelist(payload, len, &off, &list, &nlen) || !namelist_contains(list, nlen, ALG_COMP))
736 {
737 return -1;
738 }
739#endif
740
742 return 0;
743}
744
745int ssh_extinfo_build(uint8_t *out, size_t *len, size_t cap)
746{
747 // byte SSH_MSG_EXT_INFO || uint32 nr-extensions || (string name, string value)*
748 Writer w = {out, cap, 0, true};
749 w_u8(w, SSH_MSG_EXT_INFO);
750 w_u32(w, 1); // one extension
751 w_namelist(w, "server-sig-algs"); // extension name
752 // Accepted client public-key signature algorithms for userauth. All are always
753 // verifiable (independent of which host key we hold); ordered by our preference so a
754 // modern client picks the steered-to type. A client uses this to choose a key to offer.
755 // Both RSA hashes are offered (rsa-sha2-512 first, RFC 8332); pc_rsa_verify picks the
756 // hash from the client's chosen algorithm name. ecdsa-sha2-nistp256 (RFC 5656) and
757 // ssh-ed25519 are also verifiable, so all four are advertised in preference order.
758 const char *siglist = s_sshtr.prefer_rsa ? "rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256,ssh-ed25519"
759 : "ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256";
760 w_namelist(w, siglist); // value: accepted client-sig algorithms
761 if (!w.ok)
762 {
763 return -1;
764 }
765 *len = w.len;
766 return 0;
767}
768
769// ---------------------------------------------------------------------------
770// Exchange hash H (RFC 4253 §8) - streamed into the negotiated KEX hash (SHA-256 or SHA-512 via
771// SshKexHash), no large buffer.
772// ---------------------------------------------------------------------------
773
774// Hash a 4-byte big-endian length prefix.
775static void hash_u32(SshKexHash *h, uint32_t v)
776{
777 uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
778 ssh_kexhash_update(h, b, 4);
779}
780
781// Hash an SSH string: uint32 length + raw bytes.
782static void hash_string(SshKexHash *h, const uint8_t *data, size_t len)
783{
784 hash_u32(h, (uint32_t)len);
785 ssh_kexhash_update(h, data, len);
786}
787
788// Hash an SSH mpint from a fixed-width big-endian integer: strip leading zero
789// bytes, prepend a 0x00 if the top bit is set (to keep it positive).
790static void hash_mpint(SshKexHash *h, const uint8_t *be, size_t len)
791{
792 size_t off = 0;
793 while (off < len && be[off] == 0)
794 {
795 off++;
796 }
797 if (off == len)
798 {
799 // Value is zero → mpint is an empty string.
800 hash_u32(h, 0);
801 return;
802 }
803 bool pad = (be[off] & 0x80u) != 0;
804 uint32_t mlen = (uint32_t)(len - off) + (pad ? 1u : 0u);
805 hash_u32(h, mlen);
806 if (pad)
807 {
808 uint8_t zero = 0;
809 ssh_kexhash_update(h, &zero, 1);
810 }
811 ssh_kexhash_update(h, be + off, len - off);
812}
813
814// The exchange-hash algorithm for a KEX method: SHA-512 for sntrup761x25519-sha512, else SHA-256.
815static inline bool kex_is_sha512(SshKexAlg a)
816{
818}
819
820// Method-neutral exchange hash. The client/server public values are hashed as SSH
821// strings for an ECDH KEX (Q_C, Q_S; RFC 8731) or the PQ/T hybrid (C_INIT, S_REPLY), or as mpints
822// for a finite-field DH KEX (e, f; RFC 4253 §8). K is an mpint for the classical methods but a plain
823// string for the hybrid (its K is a fixed-length HASH output, RFC 4251 §5 / draft-ietf-sshm). cpub/
824// spub are big-endian, right-aligned in their buffers, so hash_mpint / hash_string produce the
825// canonical minimal encoding.
826// @p out holds SSH_KEXHASH_MAX_LEN; the exchange-hash length (32 or 64) is written to @p out_len and
827// selected by @p is512 (the negotiated KEX's hash). Returns 0, or -1 on a bad slot.
828static int compute_exchange_hash(uint8_t i, bool pub_is_string, const uint8_t *cpub, size_t cpub_len,
829 const uint8_t *spub, size_t spub_len, const uint8_t *k_be, size_t k_len,
830 const uint8_t *ks, size_t ks_len, uint8_t out[SSH_KEXHASH_MAX_LEN], size_t *out_len,
831 bool k_is_string, bool is512)
832{
833 if (i >= MAX_SSH_CONNS)
834 {
835 return -1;
836 }
837 SshSession *s = &ssh_sess[i];
838
839 static const char *const v_s = SSH_SERVER_VERSION;
840
841 SshKexHash h;
842 ssh_kexhash_init(&h, is512);
843 hash_string(&h, (const uint8_t *)s->v_c, s->v_c_len); // V_C
844 hash_string(&h, (const uint8_t *)v_s, sizeof(SSH_SERVER_VERSION) - 1); // V_S
845 hash_string(&h, s->i_c, s->i_c_len); // I_C
846 hash_string(&h, s->i_s, s->i_s_len); // I_S
847 hash_string(&h, ks, ks_len); // K_S
848 if (pub_is_string)
849 {
850 hash_string(&h, cpub, cpub_len); // Q_C
851 hash_string(&h, spub, spub_len); // Q_S
852 }
853 else
854 {
855 hash_mpint(&h, cpub, cpub_len); // e
856 hash_mpint(&h, spub, spub_len); // f
857 }
858 if (k_is_string)
859 {
860 hash_string(&h, k_be, k_len); // hybrid: K is a fixed-length HASH output (RFC 4251 string)
861 }
862 else
863 {
864 hash_mpint(&h, k_be, k_len); // classical: K is an mpint
865 }
866 *out_len = ssh_kexhash_final(&h, out);
867 return 0;
868}
869
870int ssh_kex_exchange_hash(uint8_t i, const uint8_t *e_be, const uint8_t *f_be, const uint8_t *k_be, const uint8_t *ks,
871 size_t ks_len, uint8_t out[PC_SHA256_DIGEST_LEN])
872{
873 size_t out_len = 0; // dh-group14-sha256 is always SHA-256
874 return compute_exchange_hash(i, false, e_be, 256, f_be, 256, k_be, 256, ks, ks_len, out, &out_len, false, false);
875}
876
877// ---------------------------------------------------------------------------
878// KEXDH (RFC 4253 §8)
879// ---------------------------------------------------------------------------
880
881int ssh_kexdh_parse_init(const uint8_t *payload, size_t len, uint8_t e_be[256])
882{
883 if (len < 1 + 4 || payload[0] != SSH_MSG_KEXDH_INIT)
884 {
885 return -1;
886 }
887
888 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
889 (uint32_t)payload[4];
890 if ((size_t)5 + n > len)
891 {
892 return -1;
893 }
894
895 const uint8_t *m = payload + 5;
896 size_t off = 0;
897 while (off < n && m[off] == 0) // strip sign/leading-zero bytes
898 {
899 off++;
900 }
901 size_t vlen = n - off;
902 if (vlen > 256)
903 {
904 return -1; // e exceeds 2048 bits
905 }
906
907 memset(e_be, 0, 256);
908 memcpy(e_be + (256 - vlen), m + off, vlen);
909 return 0;
910}
911
912int ssh_kexdh_build_reply(const uint8_t *ks, size_t ks_len, const uint8_t *f_be, const uint8_t *sig, size_t sig_len,
913 uint8_t *out, size_t *out_len, size_t cap)
914{
915 Writer w = {out, cap, 0, true};
916 w_u8(w, SSH_MSG_KEXDH_REPLY);
917 w_string(w, ks, ks_len); // K_S
918 w_mpint(w, f_be, 256); // f
919
920 // signature = string( string("rsa-sha2-256") || string(sig) )
921 uint32_t inner = 4 + 12 + 4 + (uint32_t)sig_len;
922 w_u32(w, inner);
923 w_string(w, (const uint8_t *)"rsa-sha2-256", 12);
924 w_string(w, sig, sig_len);
925
926 if (!w.ok)
927 {
928 return -1;
929 }
930 *out_len = w.len;
931 return 0;
932}
933
934// Parse SSH_MSG_KEX_ECDH_INIT (msg 30, shares the number with KEXDH_INIT):
935// byte(30) || string(Q_C). Q_C must be exactly 32 bytes for X25519 (RFC 8731).
936static int parse_ecdh_init(const uint8_t *payload, size_t len, uint8_t qc[32])
937{
938 if (len < 1 + 4 || payload[0] != SSH_MSG_KEXDH_INIT)
939 {
940 return -1;
941 }
942 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
943 (uint32_t)payload[4];
944 if (n != 32 || (size_t)5 + n > len)
945 {
946 return -1;
947 }
948 memcpy(qc, payload + 5, 32);
949 return 0;
950}
951
952// Parse SSH_MSG_KEX_ECDH_INIT for ecdh-sha2-nistp256 (RFC 5656 §4): byte(30) || string(Q_C),
953// where Q_C is the 65-byte uncompressed client point 0x04 || X || Y.
954static int parse_ecdh_init_p256(const uint8_t *payload, size_t len, uint8_t qc[PC_ECDSA_P256_PUB_LEN])
955{
956 if (len < 1 + 4 || payload[0] != SSH_MSG_KEXDH_INIT)
957 {
958 return -1;
959 }
960 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
961 (uint32_t)payload[4];
962 if (n != PC_ECDSA_P256_PUB_LEN || (size_t)5 + n > len)
963 {
964 return -1;
965 }
966 memcpy(qc, payload + 5, PC_ECDSA_P256_PUB_LEN);
967 return 0;
968}
969
970// Encode the server host-key blob K_S for the negotiated host-key algorithm.
971// rsa-sha2-256/512 → "ssh-rsa" blob (ssh_rsa_encode_pubkey)
972// ssh-ed25519 → string("ssh-ed25519") || string(pub32) (RFC 8709 §4)
973// ecdsa-sha2-nistp256 → string(name) || string("nistp256") || string(Q) (RFC 5656 §3.1)
974static int encode_hostkey(uint8_t i, uint8_t *ks, size_t *ks_len, size_t cap)
975{
976 if (ssh_sess[i].hostkey_alg == SshHostkeyAlg::SSH_HOSTKEY_ED25519)
977 {
978 Writer w = {ks, cap, 0, true};
979 w_string(w, (const uint8_t *)HOSTKEY_ED, sizeof(HOSTKEY_ED) - 1);
980 w_string(w, s_sshtr.ed_pub, 32);
981 if (!w.ok) // GCOVR_EXCL_LINE the ed25519 blob (~51B) always fits the RSA-sized ks buffer
982 {
983 return -1; // GCOVR_EXCL_LINE
984 }
985 *ks_len = w.len;
986 return 0;
987 }
989 {
990 Writer w = {ks, cap, 0, true};
991 w_string(w, (const uint8_t *)HOSTKEY_ECDSA, sizeof(HOSTKEY_ECDSA) - 1);
992 w_string(w, (const uint8_t *)"nistp256", 8); // RFC 5656 curve identifier
993 w_string(w, s_sshtr.ecdsa_pub, PC_ECDSA_P256_PUB_LEN);
994 if (!w.ok) // GCOVR_EXCL_LINE the ecdsa blob (~104B) always fits the RSA-sized ks buffer
995 {
996 return -1; // GCOVR_EXCL_LINE
997 }
998 *ks_len = w.len;
999 return 0;
1000 }
1001 return ssh_rsa_encode_pubkey(ks, ks_len, cap);
1002}
1003
1004// Sign the exchange hash H with the negotiated host key. Writes the raw signature (no
1005// SSH framing) plus its algorithm name; the caller wraps it as the signature blob.
1006static int sign_hash(uint8_t i, const uint8_t *H, size_t h_len, uint8_t *sig, size_t *sig_len, size_t sig_cap,
1007 const char **sig_name)
1008{
1009 if (ssh_sess[i].hostkey_alg == SshHostkeyAlg::SSH_HOSTKEY_ED25519)
1010 {
1011 if (sig_cap < 64) // GCOVR_EXCL_LINE the caller's sig buffer is PC_RSA_SIG_BYTES (256) >= 64
1012 {
1013 return -1; // GCOVR_EXCL_LINE
1014 }
1015 pc_ed25519_sign(sig, H, h_len, s_sshtr.ed_seed);
1016 *sig_len = 64;
1017 *sig_name = HOSTKEY_ED; // "ssh-ed25519"
1018 return 0;
1019 }
1021 {
1022 uint8_t raw[PC_ECDSA_P256_SIG_LEN]; // r || s (32 + 32)
1023 if (!pc_ecdsa_p256_sign(raw, H, h_len, s_sshtr.ecdsa_priv))
1024 {
1025 return -1; // GCOVR_EXCL_LINE key is available (negotiated) and sign is infallible for a valid d
1026 }
1027 // ECDSA signature blob is mpint(r) || mpint(s) (RFC 5656 §3.1.2).
1028 Writer w = {sig, sig_cap, 0, true};
1029 w_mpint(w, raw, PC_ECDSA_P256_COORD_LEN);
1031 if (!w.ok) // GCOVR_EXCL_LINE the mpint blob (~74B) always fits the 256B sig buffer
1032 {
1033 return -1; // GCOVR_EXCL_LINE
1034 }
1035 *sig_len = w.len;
1036 *sig_name = HOSTKEY_ECDSA; // "ecdsa-sha2-nistp256"
1037 return 0;
1038 }
1039 // rsa-sha2-512 and rsa-sha2-256 share the one "ssh-rsa" key; the negotiated
1040 // algorithm only chooses the signature hash (RFC 8332).
1043 if (sig_cap < PC_RSA_SIG_BYTES || // GCOVR_EXCL_LINE neither operand can be true: the caller's buffer is
1044 ssh_rsa_sign(H, h_len, rh, sig) != 0) // GCOVR_EXCL_LINE sig buffer is 256B and the negotiated
1045 {
1046 return -1; // GCOVR_EXCL_LINE RSA key is loaded (available), so neither the size nor the sign can fail
1047 }
1048 *sig_len = PC_RSA_SIG_BYTES;
1049 *sig_name = sha512 ? HOSTKEY_RSA_SHA512 : HOSTKEY_RSA_SHA256;
1050 return 0;
1051}
1052
1053// Assemble SSH_MSG_KEXDH_REPLY (== KEX_ECDH_REPLY / KEX_HYBRID_REPLY, msg 31):
1054// byte(31) || string(K_S) || (mpint f | string Q_S | string S_REPLY) || string( string(sig_name) || string(sig) )
1055static int build_kex_reply(uint8_t i, const uint8_t *ks, size_t ks_len, const uint8_t *spub, size_t spub_len,
1056 const char *sig_name, const uint8_t *sig, size_t sig_len, uint8_t *out, size_t *out_len,
1057 size_t cap)
1058{
1059 Writer w = {out, cap, 0, true};
1060 w_u8(w, SSH_MSG_KEXDH_REPLY);
1061 w_string(w, ks, ks_len); // K_S
1062 if (ssh_sess[i].kex_alg == SshKexAlg::SSH_KEX_DH_GROUP14)
1063 {
1064 w_mpint(w, spub, spub_len); // f (mpint)
1065 }
1066 else
1067 {
1068 w_string(w, spub, spub_len); // Q_S (curve25519) or S_REPLY (hybrid), a raw string
1069 }
1070 uint32_t nl = (uint32_t)strnlen(sig_name, w.cap);
1071 w_u32(w, 4 + nl + 4 + (uint32_t)sig_len); // signature blob length
1072 w_string(w, (const uint8_t *)sig_name, nl);
1073 w_string(w, sig, sig_len);
1074 if (!w.ok)
1075 {
1076 return -1;
1077 }
1078 *out_len = w.len;
1079 return 0;
1080}
1081
1082int ssh_kex_generate(uint8_t i)
1083{
1084 if (i >= MAX_SSH_CONNS)
1085 {
1086 return -1;
1087 }
1088 SshKexAlg a = ssh_sess[i].kex_alg;
1089 bool curve = (a == SshKexAlg::SSH_KEX_CURVE25519);
1090#if PC_ENABLE_PQC_KEX
1091 // both PQ/T hybrids run X25519 as the classical half.
1093#endif
1094 if (curve)
1095 {
1096 // X25519 ephemeral: random 32-byte scalar, public = X25519(scalar, base point).
1097#ifdef PC_SSH_KEX_BENCH
1098 int64_t kexgen_t0 = esp_timer_get_time();
1099#endif
1100 ssh_rng_fill(ssh_sess[i].ecdh_sk, 32);
1101 pc_x25519_base(ssh_sess[i].ecdh_pk, ssh_sess[i].ecdh_sk);
1102#ifdef PC_SSH_KEX_BENCH
1103 pc_ssh_kex_bench.last_kexgen_us = (long long)(esp_timer_get_time() - kexgen_t0);
1104#endif
1105 return 0;
1106 }
1108 {
1109 // P-256 ECDH ephemeral: a random scalar d in [1, n) stored in ecdh_sk. The 65-byte public
1110 // point Q_S = d*G is re-derived in ssh_kexdh_handle (avoids a curve-specific session field).
1111 // Re-draw on the negligible chance a raw 32-byte value is 0 or >= n (an invalid P-256 scalar).
1112 uint8_t qtmp[PC_ECDSA_P256_PUB_LEN];
1113 for (int t = 0; t < 8; t++) // GCOVR_EXCL_LINE the loop can only run past its first pass if the RNG
1114 { // hands back an invalid P-256 scalar, which no host build can provoke
1115 ssh_rng_fill(ssh_sess[i].ecdh_sk, 32);
1116 if (pc_ecdsa_p256_pubkey(qtmp, ssh_sess[i].ecdh_sk)) // GCOVR_EXCL_LINE a random 32-byte value is a
1117 {
1118 return 0; // valid scalar with overwhelming probability
1119 }
1120 }
1121 return -1; // GCOVR_EXCL_LINE a random 32-byte scalar is a valid P-256 key with overwhelming probability
1122 }
1123 return ssh_dh_generate(i);
1124}
1125
1126#if PC_ENABLE_PQC_KEX
1127// mlkem768x25519-sha256 (draft-ietf-sshm-mlkem-hybrid-kex): from the client's SSH_MSG_KEX_HYBRID_INIT
1128// (byte 30 || string C_INIT, C_INIT = ek(1184) || Q_C(32)), ML-KEM-Encaps to the peer's key and X25519
1129// against Q_C, then combine K = SHA256(K_PQ || K_CL). Writes S_REPLY = ciphertext(1088) || Q_S(32) and
1130// the 32-byte shared secret. Returns 0, or -1 on a malformed C_INIT, bad ML-KEM key, or low-order point.
1131static int hybrid_mlkem_x25519(uint8_t i, const uint8_t *payload, size_t len, uint8_t s_reply[MLKEM768_CT_BYTES + 32],
1132 uint8_t k_out[32])
1133{
1134 if (len < 1 + 4 || payload[0] != SSH_MSG_KEXDH_INIT)
1135 {
1136 return -1;
1137 }
1138 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
1139 (uint32_t)payload[4];
1140 if (n != MLKEM768_EK_BYTES + 32 || (size_t)5 + n > len)
1141 {
1142 return -1;
1143 }
1144 const uint8_t *ek = payload + 5; // C_PK2: ML-KEM-768 encapsulation key
1145 const uint8_t *qc = payload + 5 + MLKEM768_EK_BYTES; // C_PK1: client X25519 public
1146
1147 uint8_t m[32];
1148 ssh_rng_fill(m, sizeof(m));
1149 uint8_t k_pq[32];
1150 bool ok = pc_mlkem768_encaps(ek, m, s_reply, k_pq); // ciphertext -> s_reply[0..1087]
1151 pc_secure_wipe(m, sizeof(m));
1152 if (!ok)
1153 {
1154 return -1; // malformed encapsulation key (FIPS 203 modulus check)
1155 }
1156
1157 uint8_t k_cl[32];
1158 pc_x25519(k_cl, ssh_sess[i].ecdh_sk, qc);
1159 uint8_t zacc = 0;
1160 for (int b = 0; b < 32; b++)
1161 {
1162 zacc |= k_cl[b];
1163 }
1164 if (zacc == 0) // low-order X25519 point (RFC 7748 §6.1)
1165 {
1166 pc_secure_wipe(k_pq, sizeof(k_pq));
1167 pc_secure_wipe(k_cl, sizeof(k_cl));
1168 return -1;
1169 }
1170 memcpy(s_reply + MLKEM768_CT_BYTES, ssh_sess[i].ecdh_pk, 32); // S_PK1: server X25519 public
1171
1172 pc_sha256_ctx hc;
1173 pc_sha256_init(&hc);
1174 pc_sha256_update(&hc, k_pq, sizeof(k_pq)); // K = SHA256(K_PQ || K_CL) (RFC 9370 concat combiner)
1175 pc_sha256_update(&hc, k_cl, sizeof(k_cl));
1176 pc_sha256_final(&hc, k_out);
1177 pc_secure_wipe(k_pq, sizeof(k_pq));
1178 pc_secure_wipe(k_cl, sizeof(k_cl));
1179 return 0;
1180}
1181#endif // PC_ENABLE_PQC_KEX (ML-KEM hybrid)
1182
1183#if PC_ENABLE_SSH_SNTRUP761
1184// sntrup761x25519-sha512@openssh.com: from C_INIT (byte 30 || string, string = sntrup761_pk(1158) ||
1185// Q_C(32)), sntrup761-Encaps to the peer's key and X25519 against Q_C, then combine K = SHA512(K_PQ ||
1186// K_CL) (64 bytes). Writes S_REPLY = ciphertext(1039) || Q_S(32) and the 64-byte shared secret. Returns
1187// 0, or -1 on a malformed C_INIT or a low-order X25519 point.
1188static int hybrid_sntrup761_x25519(uint8_t i, const uint8_t *payload, size_t len,
1189 uint8_t s_reply[PC_SNTRUP761_CT_BYTES + 32], uint8_t k_out[64])
1190{
1191 if (len < 1 + 4 || payload[0] != SSH_MSG_KEXDH_INIT)
1192 {
1193 return -1;
1194 }
1195 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
1196 (uint32_t)payload[4];
1197 if (n != PC_SNTRUP761_PK_BYTES + 32 || (size_t)5 + n > len)
1198 {
1199 return -1;
1200 }
1201 const uint8_t *pk = payload + 5; // C_PK2: sntrup761 public key
1202 const uint8_t *qc = payload + 5 + PC_SNTRUP761_PK_BYTES; // C_PK1: client X25519 public
1203
1204 uint8_t k_pq[PC_SNTRUP761_SS_BYTES];
1205 pc_sntrup761_enc(pk, s_reply, k_pq); // ciphertext -> s_reply[0..1038], shared -> k_pq
1206
1207 uint8_t k_cl[32];
1208 pc_x25519(k_cl, ssh_sess[i].ecdh_sk, qc);
1209 uint8_t zacc = 0;
1210 for (int b = 0; b < 32; b++)
1211 {
1212 zacc |= k_cl[b];
1213 }
1214 if (zacc == 0) // low-order X25519 point (RFC 7748 §6.1)
1215 {
1216 pc_secure_wipe(k_pq, sizeof(k_pq));
1217 pc_secure_wipe(k_cl, sizeof(k_cl));
1218 return -1;
1219 }
1220 memcpy(s_reply + PC_SNTRUP761_CT_BYTES, ssh_sess[i].ecdh_pk, 32); // S_PK1: server X25519 public
1221
1222 pc_sha512_ctx hc;
1223 pc_sha512_init(&hc);
1224 pc_sha512_update(&hc, k_pq, sizeof(k_pq)); // K = SHA512(K_PQ || K_CL) (RFC 9370 concat combiner)
1225 pc_sha512_update(&hc, k_cl, sizeof(k_cl));
1226 pc_sha512_final(&hc, k_out);
1227 pc_secure_wipe(k_pq, sizeof(k_pq));
1228 pc_secure_wipe(k_cl, sizeof(k_cl));
1229 return 0;
1230}
1231#endif // PC_ENABLE_SSH_SNTRUP761
1232
1233int ssh_kexdh_handle(uint8_t i, const uint8_t *payload, size_t len, uint8_t *reply_out, size_t *reply_len, size_t cap)
1234{
1235 if (i >= MAX_SSH_CONNS)
1236 {
1237 return -1;
1238 }
1239 SshSession *s = &ssh_sess[i];
1240#ifdef PC_SSH_KEX_BENCH
1241 int64_t kexreply_t0 = esp_timer_get_time();
1242#endif
1243
1244 // 1. Shared secret K + the two public values, per negotiated KEX method. cpub_p / spub_p point at
1245 // the values hashed into H (local buffers for DH / curve, the larger C_INIT / S_REPLY blobs for
1246 // the hybrid); k_hash / k_hash_len select K's encoding (an mpint for DH / curve, a fixed 32-byte
1247 // string for the hybrid). k_be holds K right-aligned so hash_mpint / the KDF strip to minimal.
1248 uint8_t k_be[256];
1249 memset(k_be, 0, sizeof(k_be));
1250 uint8_t cpub[256];
1251 uint8_t spub[256]; // client / server public value (right-aligned) for DH / curve25519
1252 const uint8_t *cpub_p = cpub;
1253 const uint8_t *spub_p = spub;
1254 size_t cpub_len = 256;
1255 size_t spub_len = 256;
1256 const uint8_t *k_hash = k_be;
1257 size_t k_hash_len = 256;
1258 bool pub_is_string = false;
1259 bool k_is_string = false;
1260#if PC_ENABLE_PQC_KEX
1261 uint8_t s_reply[MLKEM768_CT_BYTES + 32]; // hybrid S_REPLY = ciphertext(1088) || Q_S(32)
1262#endif
1263
1265 {
1266 // curve25519-sha256 (RFC 8731): K = X25519(sk, Q_C); Q_C/Q_S hashed as strings.
1267 uint8_t qc[32];
1268 uint8_t kk[32];
1269 if (parse_ecdh_init(payload, len, qc) != 0)
1270 {
1271 return -1;
1272 }
1273 pc_x25519(kk, s->ecdh_sk, qc);
1274 // Reject a low-order client point (all-zero shared secret) - RFC 7748 §6.1.
1275 uint8_t zacc = 0;
1276 for (int b = 0; b < 32; b++)
1277 {
1278 zacc |= kk[b];
1279 }
1280 if (zacc == 0)
1281 {
1282 pc_secure_wipe(kk, sizeof(kk));
1283 return -1;
1284 }
1285 memcpy(k_be + (256 - 32), kk, 32);
1286 memcpy(cpub, qc, 32);
1287 memcpy(spub, s->ecdh_pk, 32);
1288 cpub_len = spub_len = 32;
1289 pub_is_string = true;
1290 pc_secure_wipe(kk, sizeof(kk));
1291 }
1292#if PC_ENABLE_PQC_KEX
1294 {
1295 // mlkem768x25519-sha256: K = SHA256(K_PQ || K_CL); C_INIT / S_REPLY and K hashed as strings.
1296 if (hybrid_mlkem_x25519(i, payload, len, s_reply, k_be + (256 - 32)) != 0)
1297 {
1298 return -1;
1299 }
1300 cpub_p = payload + 5; // C_INIT (ek || Q_C), hashed verbatim as a string
1301 cpub_len = MLKEM768_EK_BYTES + 32;
1302 spub_p = s_reply; // S_REPLY (ciphertext || Q_S)
1303 spub_len = MLKEM768_CT_BYTES + 32;
1304 k_hash = k_be + (256 - 32); // K is exactly 32 bytes, string-encoded (not mpint)
1305 k_hash_len = 32;
1306 pub_is_string = true;
1307 k_is_string = true;
1308 }
1309#endif // PC_ENABLE_PQC_KEX (ML-KEM dispatch)
1310#if PC_ENABLE_SSH_SNTRUP761
1312 {
1313 // sntrup761x25519-sha512: K = SHA512(K_PQ || K_CL) (64 bytes); C_INIT / S_REPLY and K are strings.
1314 if (hybrid_sntrup761_x25519(i, payload, len, s_reply, k_be + (256 - 64)) != 0)
1315 {
1316 return -1;
1317 }
1318 cpub_p = payload + 5; // C_INIT (sntrup761_pk || Q_C), hashed verbatim as a string
1319 cpub_len = PC_SNTRUP761_PK_BYTES + 32;
1320 spub_p = s_reply; // S_REPLY (ciphertext || Q_S)
1321 spub_len = PC_SNTRUP761_CT_BYTES + 32;
1322 k_hash = k_be + (256 - 64); // K is exactly 64 bytes (SHA-512), string-encoded
1323 k_hash_len = 64;
1324 pub_is_string = true;
1325 k_is_string = true;
1326 }
1327#endif // PC_ENABLE_SSH_SNTRUP761
1329 {
1330 // ecdh-sha2-nistp256 (RFC 5656 §4): K = X(d_S * Q_C). Q_C/Q_S are 65-byte point strings; K an mpint.
1331 uint8_t qc[PC_ECDSA_P256_PUB_LEN];
1332 if (parse_ecdh_init_p256(payload, len, qc) != 0)
1333 {
1334 return -1;
1335 }
1336 uint8_t qs[PC_ECDSA_P256_PUB_LEN];
1337 uint8_t kk[PC_ECDSA_P256_COORD_LEN];
1338 // Re-derive our ephemeral public Q_S, then the shared secret. pc_ecdsa_p256_ecdh validates
1339 // Q_C is on-curve and the product is not the identity (RFC 5656 §4 point checks).
1340 if (!pc_ecdsa_p256_pubkey(qs, s->ecdh_sk) || !pc_ecdsa_p256_ecdh(kk, qc, s->ecdh_sk))
1341 {
1342 return -1;
1343 }
1344 memcpy(k_be + (256 - PC_ECDSA_P256_COORD_LEN), kk, PC_ECDSA_P256_COORD_LEN);
1345 memcpy(cpub, qc, PC_ECDSA_P256_PUB_LEN);
1346 memcpy(spub, qs, PC_ECDSA_P256_PUB_LEN);
1347 cpub_len = PC_ECDSA_P256_PUB_LEN;
1348 spub_len = PC_ECDSA_P256_PUB_LEN;
1349 pub_is_string = true;
1350 pc_secure_wipe(kk, sizeof(kk));
1351 }
1352 else
1353 {
1354 // diffie-hellman-group14-sha256 (RFC 4253 §8): K = e^y mod p; e/f are mpints.
1355 uint8_t e_be[256];
1356 if (ssh_kexdh_parse_init(payload, len, e_be) != 0)
1357 {
1358 return -1;
1359 }
1360 pc_bignum e;
1361 bn_from_bytes(&e, e_be, 256);
1362 if (bn_dh_validate(&e) != 0)
1363 {
1364 return -1;
1365 }
1366 pc_bignum K;
1367 bn_expmod_group14(&K, &e, &ssh_dh[i].y);
1368 bn_to_bytes(k_be, &K);
1369 pc_secure_wipe(&K, sizeof(K));
1370 memcpy(cpub, e_be, 256);
1371 bn_to_bytes(spub, &ssh_dh[i].f);
1372 }
1373
1374 // 2. Host-key blob K_S (per negotiated host-key algorithm).
1375 uint8_t ks[SSH_RSA_PUBKEY_BLOB_MAX];
1376 size_t ks_len = 0;
1377 if (encode_hostkey(i, ks, &ks_len, sizeof(ks)) != 0) // GCOVR_EXCL_LINE encode_hostkey cannot fail: ks is
1378 { // GCOVR_EXCL_LINE SSH_RSA_PUBKEY_BLOB_MAX, sized for either blob
1379 pc_secure_wipe(k_be, sizeof(k_be)); // GCOVR_EXCL_LINE
1380 return -1; // GCOVR_EXCL_LINE
1381 }
1382
1383 // 3. Exchange hash H (SHA-256 or SHA-512 per the KEX method); capture the session id on first KEX.
1384 const bool is512 = kex_is_sha512(s->kex_alg);
1385 uint8_t H[SSH_KEXHASH_MAX_LEN];
1386 size_t h_len = 0;
1387 compute_exchange_hash(i, pub_is_string, cpub_p, cpub_len, spub_p, spub_len, k_hash, k_hash_len, ks, ks_len, H,
1388 &h_len, k_is_string, is512);
1389 if (!s->have_session_id)
1390 {
1391 memcpy(s->session_id, H, h_len);
1392 s->session_id_len = (uint8_t)h_len;
1393 s->have_session_id = true;
1394 }
1395
1396 // 4. Sign H with the negotiated host key (rsa-sha2-512/256 or ssh-ed25519).
1397 uint8_t sig[PC_RSA_SIG_BYTES]; // 256 bytes: fits an RSA-2048 sig and a 64-byte ed25519 sig
1398 size_t sig_len = 0;
1399 const char *sig_name = nullptr;
1400 if (sign_hash(i, H, h_len, sig, &sig_len, sizeof(sig), &sig_name) != 0) // GCOVR_EXCL_LINE cannot fail here:
1401 { // GCOVR_EXCL_LINE 256B sig + loaded key
1402 pc_secure_wipe(k_be, sizeof(k_be)); // GCOVR_EXCL_LINE
1403 return -1; // GCOVR_EXCL_LINE
1404 }
1405
1406 // 5. Assemble the reply, then derive the six session keys (id fixed at first KEX's H).
1407 if (build_kex_reply(i, ks, ks_len, spub_p, spub_len, sig_name, sig, sig_len, reply_out, reply_len, cap) != 0)
1408 {
1409 pc_secure_wipe(k_be, sizeof(k_be));
1410 return -1;
1411 }
1412 ssh_dh_derive_keys_sid(i, k_be, H, s->session_id, s->cipher_alg, s->mac_alg, k_is_string, h_len, s->session_id_len,
1413 is512);
1414 pc_secure_wipe(k_be, sizeof(k_be));
1415
1417#ifdef PC_SSH_KEX_BENCH
1418 pc_ssh_kex_bench.last_kexreply_us = (long long)(esp_timer_get_time() - kexreply_t0);
1419 pc_ssh_kex_bench.kex_count++;
1420#endif
1421 return 0;
1422}
1423
1424void ssh_newkeys_sent(uint8_t i)
1425{
1426 if (i >= MAX_SSH_CONNS)
1427 {
1428 return;
1429 }
1430 // We have emitted our SSH_MSG_NEWKEYS: our outbound direction is now encrypted (RFC 4253 sec 7.3).
1431 ssh_pkt[i].enc_out = true;
1432#if PC_ENABLE_SSH_ZLIB
1433 // "zlib" (non-delayed) starts its s2c (outbound) stream here; idempotent, so a re-key does not restart it.
1434 ssh_comp_on_newkeys(i);
1435#endif
1436}
1437
1439{
1440 if (i >= MAX_SSH_CONNS)
1441 {
1442 return;
1443 }
1444 // We have received the peer's SSH_MSG_NEWKEYS: our inbound direction is now encrypted. Both directions
1445 // are keyed once we get here (the server always sends its NEWKEYS first), so the KEX is complete.
1446 ssh_pkt[i].enc_in = true;
1447 ssh_pkt[i].kex_active = false;
1448 // On the first KEX advance to the service phase; on a re-key the connection
1449 // is already authenticated, so resume the open (channel) phase.
1451 // Reset the re-key timer: the volume/time budget is measured from this completed KEX.
1453}
1454
1455bool ssh_rekey_needed(uint8_t i)
1456{
1457 if (i >= MAX_SSH_CONNS)
1458 {
1459 return false;
1460 }
1462}
1463
1464bool ssh_rekey_due(uint32_t seq_send, uint32_t seq_recv, uint32_t elapsed_ms, uint32_t pkt_threshold,
1465 uint32_t time_threshold_ms)
1466{
1467 if (seq_send >= pkt_threshold || seq_recv >= pkt_threshold)
1468 {
1469 return true; // volume budget (RFC 4253 sec 9: ~1 GB)
1470 }
1471 if (time_threshold_ms && elapsed_ms >= time_threshold_ms)
1472 {
1473 return true; // time budget (~1 hour)
1474 }
1475 return false;
1476}
1477
1478int ssh_transport_begin_rekey(uint8_t i, uint8_t *out, size_t *out_len, size_t cap)
1479{
1480 if (i >= MAX_SSH_CONNS)
1481 {
1482 return -1;
1483 }
1484 // Fresh server KEXINIT (re-stores I_S for the new exchange hash).
1485 if (ssh_kexinit_build(i, out, out_len, cap) != 0)
1486 {
1487 return -1;
1488 }
1489 // New ephemeral for forward secrecy across the re-key (re-generated for the finally
1490 // negotiated method once the peer's KEXINIT arrives; see the KEXINIT dispatch).
1491 if (ssh_kex_generate(i) != 0) // GCOVR_EXCL_LINE i < MAX_SSH_CONNS is checked above and ssh_kex_generate only
1492 {
1493 return -1; // GCOVR_EXCL_LINE fails for i >= MAX_SSH_CONNS
1494 }
1496 return 0;
1497}
void bn_to_bytes(uint8_t bytes[256], const pc_bignum *in)
Write a pc_bignum as a 256-byte big-endian array.
Definition bignum.cpp:96
void bn_from_bytes(pc_bignum *out, const uint8_t *bytes, size_t len)
Read a big-endian byte array of len bytes into a pc_bignum.
Definition bignum.cpp:84
int bn_dh_validate(const pc_bignum *v)
Validate a received DH public value.
Definition bignum.cpp:125
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
bool pc_ecdsa_p256_ecdh(uint8_t shared_x[PC_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[PC_ECDSA_P256_PUB_LEN], const uint8_t priv[PC_ECDSA_P256_PRIV_LEN])
P-256 ECDH: the shared-secret X coordinate of d * Q_peer (RFC 5656 §4 / RFC 5903).
Definition ecdsa.cpp:191
bool pc_ecdsa_p256_sign(uint8_t sig[PC_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen, const uint8_t priv[PC_ECDSA_P256_PRIV_LEN])
Sign mlen bytes of msg with a P-256 private key (ECDSA, SHA-256).
Definition ecdsa.cpp:127
bool pc_ecdsa_p256_pubkey(uint8_t pub[PC_ECDSA_P256_PUB_LEN], const uint8_t priv[PC_ECDSA_P256_PRIV_LEN])
Derive the uncompressed public point Q = d*G from a P-256 private scalar.
Definition ecdsa.cpp:98
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
#define PC_ECDSA_P256_COORD_LEN
P-256 coordinate length (one of X, Y).
Definition ecdsa.h:57
#define PC_ECDSA_P256_SIG_LEN
Raw ECDSA signature length: r || s (32 + 32, big-endian).
Definition ecdsa.h:61
#define PC_ECDSA_P256_PUB_LEN
P-256 uncompressed public point length: 0x04 || X || Y.
Definition ecdsa.h:59
#define PC_ECDSA_P256_PRIV_LEN
P-256 private key (scalar d) length.
Definition ecdsa.h:55
void pc_ed25519_pubkey(uint8_t pub[32], const uint8_t seed[32])
Definition ed25519.cpp:584
void pc_ed25519_sign(uint8_t sig[64], const uint8_t *msg, size_t mlen, const uint8_t seed[32])
Definition ed25519.cpp:594
Ed25519 signatures (RFC 8032) for ssh-ed25519 host keys + client auth.
ML-KEM-768 (FIPS 203): Encaps (responder) + KeyGen and Decaps (initiator).
void bn_expmod_group14(pc_bignum *out, const pc_bignum *base, const pc_bignum *exp)
#define SSH_KEXINIT_MAX
Max stored size of the CLIENT KEXINIT payload (I_C, for the exchange hash).
#define SSH_REKEY_PACKET_THRESHOLD
Re-key when either packet sequence number reaches this value.
pc_rsa_hash
Hash algorithm selecting the RSA signature scheme (RFC 8017 §9.2).
Definition rsa.h:39
@ SHA256
RSASSA-PKCS1-v1.5 with SHA-256.
@ SHA512
RSASSA-PKCS1-v1.5 with SHA-512.
#define PC_RSA_SIG_BYTES
PKCS#1 v1.5 signature size for RSA-2048 in bytes.
Definition rsa.h:31
Secure pool accessor - borrows that hold key material.
PC_CRYPTO_HOT void pc_sha256_init(pc_sha256_ctx *ctx)
Initialize a streaming SHA-256 context (ctx must not be NULL).
Definition sha256.cpp:29
void pc_sha256_final(pc_sha256_ctx *ctx, uint8_t digest[PC_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest. The context is undefined afterwards; call init() agai...
Definition sha256.cpp:48
void pc_sha256_update(pc_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha256.cpp:39
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
void pc_sha512_update(pc_sha512_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha512.cpp:38
PC_CRYPTO_HOT void pc_sha512_init(pc_sha512_ctx *ctx)
Initialize a streaming SHA-512 context (ctx must not be NULL).
Definition sha512.cpp:28
void pc_sha512_final(pc_sha512_ctx *ctx, uint8_t digest[PC_SHA512_DIGEST_LEN])
Finalize the hash and write the 64-byte digest. The context is undefined afterwards; call init() agai...
Definition sha512.cpp:47
Streamlined NTRU Prime sntrup761 KEM - responder (encapsulation) only.
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
void ssh_rng_fill(uint8_t *buf, size_t len)
Fill len bytes of buf with cryptographically random data.
Definition ssh_dh.cpp:20
void ssh_dh_derive_keys_sid(uint8_t i, const uint8_t K_be[256], const uint8_t *H, const uint8_t *session_id, uint8_t cipher_alg, uint8_t mac_alg, bool k_is_string, size_t h_len, size_t sid_len, bool is512)
Derive session keys with an explicit session id (RFC 4253 §7.2).
Definition ssh_dh.cpp:167
int ssh_dh_generate(uint8_t i)
Generate the server ephemeral DH key pair for connection slot i.
Definition ssh_dh.cpp:29
DH-group14-SHA256 key exchange (RFC 4253 §8 + RFC 8268).
#define SSH_KEXHASH_MAX_LEN
longest exchange-hash / session_id (SHA-512)
Definition ssh_kexhash.h:25
SshDhState ssh_dh[MAX_SSH_CONNS]
Pool of ephemeral DH state, one entry per MAX_SSH_CONNS.
@ 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_SHA256_ETM
hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
Definition ssh_keymat.h:130
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
Definition ssh_keymat.h:128
@ 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
SshPacketState ssh_pkt[MAX_SSH_CONNS]
Static packet state pool (BSS). One entry per SSH slot.
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_MSG_KEXINIT
Definition ssh_packet.h:102
#define SSH_MSG_KEXDH_REPLY
Definition ssh_packet.h:105
#define SSH_MSG_KEXDH_INIT
Definition ssh_packet.h:104
#define SSH_MSG_EXT_INFO
Definition ssh_packet.h:101
SshRsaPubKey ssh_host_pubkey
Static host public key (BSS). Set by pc_ssh_rsa_load_pubkey().
Definition ssh_rsa.cpp:29
int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, pc_rsa_hash hash, uint8_t sig[PC_RSA_SIG_BYTES])
Sign msg with the RSA host key (PKCS#1 v1.5, rsa-sha2-256/512).
Definition ssh_rsa.cpp:126
int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
Encode ssh_host_pubkey as the RFC 4253 §6.6 "ssh-rsa" public-key blob.
Definition ssh_rsa.cpp:230
SSH RSA host-key layer: NVS-backed host key, host-key signing, and "ssh-rsa" blob encoding.
#define SSH_RSA_PUBKEY_BLOB_MAX
Upper bound on the encoded "ssh-rsa" public-key blob (len+alg + mpint e + mpint n).
Definition ssh_rsa.h:77
bool pc_ssh_hostkey_ecdsa_available(void)
True if an ecdsa-sha2-nistp256 host key has been installed.
int ssh_kex_generate(uint8_t i)
Generate the server ephemeral for the negotiated KEX method (call after parse).
bool ssh_kex_prefer_rsa(void)
Current negotiation preference (true = prefer RSA/DH, the ESP32-accelerated path).
int ssh_transport_recv_banner(uint8_t i, const uint8_t *data, size_t len, size_t *consumed)
Feed raw bytes while awaiting the client identification string.
int ssh_extinfo_build(uint8_t *out, size_t *len, size_t cap)
Build SSH_MSG_EXT_INFO advertising server-sig-algs (RFC 8308).
int ssh_kexinit_build(uint8_t i, uint8_t *payload, size_t *len, size_t cap)
Build the server KEXINIT payload for slot i (RFC 4253 §7.1).
void pc_ssh_hostkey_ecdsa_set(const uint8_t priv[PC_ECDSA_P256_PRIV_LEN])
void ssh_newkeys_complete(uint8_t i)
Complete the NEWKEYS exchange: activate the inbound direction and advance phase.
bool ssh_rekey_needed(uint8_t i)
True if slot i has reached the re-key threshold (RFC 4253 §9).
void ssh_newkeys_sent(uint8_t i)
Activate the outbound direction after emitting our SSH_MSG_NEWKEYS.
int ssh_transport_begin_rekey(uint8_t i, uint8_t *out, size_t *out_len, size_t cap)
Begin a server-initiated re-key by emitting a fresh KEXINIT.
bool ssh_rekey_due(uint32_t seq_send, uint32_t seq_recv, uint32_t elapsed_ms, uint32_t pkt_threshold, uint32_t time_threshold_ms)
Pure re-key decision (RFC 4253 §9: "after each gigabyte ... or after each hour").
int ssh_kexdh_build_reply(const uint8_t *ks, size_t ks_len, const uint8_t *f_be, const uint8_t *sig, size_t sig_len, uint8_t *out, size_t *out_len, size_t cap)
Build SSH_MSG_KEXDH_REPLY (RFC 4253 §8, RFC 8332 §3).
void pc_ssh_hostkey_ed25519_set(const uint8_t seed[32])
Install an ssh-ed25519 host key from its 32-byte seed (RFC 8032 private key).
SshSession ssh_sess[MAX_SSH_CONNS]
Static pool of SSH session state (BSS), one per SSH slot.
void ssh_kex_set_prefer_rsa(bool prefer)
Steer KEX / host-key negotiation toward RSA + DH-group14 (default) or toward the modern curve25519 + ...
int ssh_kexinit_parse(uint8_t i, const uint8_t *payload, size_t len)
Parse and negotiate the client KEXINIT payload (RFC 4253 §7.1).
void ssh_transport_init(uint8_t i)
Reset transport state for slot i to the start of a handshake.
bool pc_ssh_hostkey_ed25519_available(void)
True if an ssh-ed25519 host key has been installed.
int ssh_kexdh_handle(uint8_t i, const uint8_t *payload, size_t len, uint8_t *reply_out, size_t *reply_len, size_t cap)
Handle KEXDH/ECDH_INIT (msg 30) end-to-end and produce the reply payload.
int ssh_kex_exchange_hash(uint8_t i, const uint8_t *e_be, const uint8_t *f_be, const uint8_t *k_be, const uint8_t *ks, size_t ks_len, uint8_t out[PC_SHA256_DIGEST_LEN])
Compute the SSH exchange hash H (RFC 4253 §8).
int ssh_transport_server_banner(uint8_t *out, size_t *out_len, size_t cap)
Write the server identification string ("SSH-2.0-…\r\n") to out.
int ssh_kexdh_parse_init(const uint8_t *payload, size_t len, uint8_t e_be[256])
Parse SSH_MSG_KEXDH_INIT, extracting the client DH value e.
SSH transport-layer protocol state machine (RFC 4253).
#define PC_SSH_KEXINIT_S_MAX
Max stored size of our own KEXINIT (I_S). Sized for the full advertised suite: the kex list (mlkem + ...
#define SSH_VERSION_MAX
Max stored length of an SSH identification string (RFC 4253 §4.2: 255).
SshHostkeyAlg
Negotiated host-key / signature algorithm.
@ SSH_HOSTKEY_RSA_SHA512
rsa-sha2-512 (same "ssh-rsa" key, SHA-512 signature; RFC 8332)
@ SSH_HOSTKEY_ECDSA_NISTP256
ecdsa-sha2-nistp256 (NIST P-256, RFC 5656)
@ SSH_HOSTKEY_RSA_SHA256
rsa-sha2-256 (HW-accelerated on ESP32)
@ SSH_HOSTKEY_ED25519
ssh-ed25519 (RFC 8032)
SshKexAlg
SSH transport/session state for one connection (BSS pool).
@ SSH_KEX_MLKEM768_X25519
mlkem768x25519-sha256 (PQ/T hybrid, draft-ietf-sshm-mlkem-hybrid-kex)
@ SSH_KEX_SNTRUP761_X25519
sntrup761x25519-sha512@openssh.com (PQ/T hybrid, SHA-512 exchange hash)
@ SSH_KEX_ECDH_NISTP256
ecdh-sha2-nistp256 (NIST P-256 ECDH, RFC 5656 §4)
@ SSH_KEX_CURVE25519
curve25519-sha256 (RFC 8731, X25519)
@ SSH_KEX_DH_GROUP14
diffie-hellman-group14-sha256 (HW-accelerated MPI on ESP32)
@ SSH_PHASE_KEXINIT
Awaiting the client KEXINIT.
@ SSH_PHASE_SERVICE
Awaiting SERVICE_REQUEST ("ssh-userauth").
@ SSH_PHASE_OPEN
Authenticated; connection/channel protocol active.
@ SSH_PHASE_NEWKEYS
Awaiting SSH_MSG_NEWKEYS.
@ SSH_PHASE_BANNER
Awaiting the client identification string.
@ SSH_PHASE_DH_INIT
Awaiting SSH_MSG_KEXDH_INIT.
#define SSH_SERVER_VERSION
Server identification string (no CR LF; appended on the wire).
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_lit(pc_sb *b, const char(&s)[N])
Append a string literal. The array parameter deduces N, so the length is a constant.
Definition strbuf.h:76
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
const char * name
A key-exchange digest bound to one of the SSH KEX hashes (SHA-256 or SHA-512).
Definition ssh_kexhash.h:29
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
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
bool loaded
True after pc_ssh_rsa_load_pubkey() succeeds.
Definition ssh_rsa.h:70
uint8_t session_id_len
session_id length (the first KEX's exchange-hash length).
bool ext_info_c
Client advertised ext-info-c (RFC 8308): send EXT_INFO.
uint8_t banner_buf[SSH_VERSION_MAX]
Accumulator for the inbound banner.
SshKexAlg kex_alg
negotiated in KEXINIT.
uint8_t ecdh_pk[32]
Server X25519 ephemeral public (curve25519 KEX only).
SshHostkeyAlg hostkey_alg
negotiated in KEXINIT.
uint16_t i_c_len
Length of i_c.
bool have_session_id
True once the first KEX completes.
SshPhase phase
Current handshake phase.
char v_c[SSH_VERSION_MAX]
Client identification string (no CR LF).
uint16_t i_s_len
Length of i_s.
bool authed
True after successful user authentication.
uint8_t mac_alg
SSH_MAC_* negotiated in KEXINIT (aes cipher only; 0 = hmac-sha2-256).
uint8_t i_s[PC_SSH_KEXINIT_S_MAX]
Server KEXINIT payload (for H).
uint16_t banner_len
Bytes buffered in banner_buf.
uint8_t i_c[SSH_KEXINIT_MAX]
Client KEXINIT payload (for H).
uint32_t last_kex_ms
pc_millis() when the last KEX completed (server-initiated re-key timer).
uint8_t ecdh_sk[32]
Server X25519 ephemeral private (curve25519 KEX only; wiped after).
uint8_t session_id[SSH_KEXHASH_MAX_LEN]
H from the first KEX (RFC 4253 §7.2); 32 or 64 bytes.
uint8_t cipher_alg
SSH_CIPHER_* negotiated in KEXINIT (0 = aes256-ctr).
uint16_t v_c_len
Length of v_c.
uint8_t ed_pub[32]
uint8_t ecdsa_priv[PC_ECDSA_P256_PRIV_LEN]
P-256 host private scalar d.
uint8_t ed_seed[32]
uint8_t ecdsa_pub[PC_ECDSA_P256_PUB_LEN]
P-256 host public point (0x04||X||Y).
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition bignum.h:92
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
char * p
Definition strbuf.h:31
Streaming SHA-256 context.
Definition sha256.h:40
Streaming SHA-512 context.
Definition sha512.h:33