19#if DETWS_ENABLE_PQC_KEX
22#if DETWS_ENABLE_SSH_ZLIB
38static const char *
const KEX_DH =
"diffie-hellman-group14-sha256";
39static const char *
const KEX_C25519 =
"curve25519-sha256";
40static const char *
const KEX_C25519_LIBSSH =
"curve25519-sha256@libssh.org";
41static const char *
const KEX_ECDH_NISTP256 =
"ecdh-sha2-nistp256";
42#if DETWS_ENABLE_PQC_KEX
43static const char *
const KEX_MLKEM768 =
"mlkem768x25519-sha256";
45static const char *
const HOSTKEY_RSA_SHA256 =
"rsa-sha2-256";
46static const char *
const HOSTKEY_RSA_SHA512 =
"rsa-sha2-512";
47static const char HOSTKEY_ED[] =
"ssh-ed25519";
48static const char HOSTKEY_ECDSA[] =
"ecdsa-sha2-nistp256";
49static const char *
const ALG_CIPHER =
"aes256-ctr";
50static const char *
const ALG_CIPHER_GCM =
"aes256-gcm@openssh.com";
53static const char *
const ALG_CIPHER_LIST =
"chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr";
54static const char *
const ALG_MAC =
"hmac-sha2-256";
57static const char *
const ALG_MAC_LIST =
"hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,"
58 "hmac-sha2-256,hmac-sha2-512";
59static const char *
const ALG_COMP =
"none";
60#if DETWS_ENABLE_SSH_ZLIB
63static const char *
const ALG_COMP_S2C =
"zlib@openssh.com,zlib,none";
65static const char *
const ALG_COMP_S2C =
"none";
68static const char *
const EXT_INFO_C =
"ext-info-c";
97 memcpy(s_sshtr.
ed_seed, seed, 32);
110 memcpy(s_sshtr.
ecdsa_priv, priv, SSH_ECDSA_P256_PRIV_LEN);
117static bool hostkey_rsa_available(
void)
125static void build_kex_list(
char *out,
size_t cap)
127 const char *c1 = KEX_C25519;
128 const char *c2 = KEX_C25519_LIBSSH;
129 const char *dh = KEX_DH;
130 const char *ec = KEX_ECDH_NISTP256;
131#if DETWS_ENABLE_PQC_KEX
134 const char *pq = KEX_MLKEM768;
136 snprintf(out, cap,
"%s,%s,%s,%s,%s,ext-info-s", pq, dh, ec, c1, c2);
138 snprintf(out, cap,
"%s,%s,%s,%s,%s,ext-info-s", pq, c1, c2, ec, dh);
141 snprintf(out, cap,
"%s,%s,%s,%s,ext-info-s", dh, ec, c1, c2);
143 snprintf(out, cap,
"%s,%s,%s,%s,ext-info-s", c1, c2, ec, dh);
146static void build_hostkey_list(
char *out,
size_t cap)
150 const bool rsa = hostkey_rsa_available();
161 cand[0] = {HOSTKEY_RSA_SHA512, rsa};
162 cand[1] = {HOSTKEY_RSA_SHA256, rsa};
163 cand[2] = {HOSTKEY_ECDSA, ec};
164 cand[3] = {HOSTKEY_ED, ed};
168 cand[0] = {HOSTKEY_ED, ed};
169 cand[1] = {HOSTKEY_ECDSA, ec};
170 cand[2] = {HOSTKEY_RSA_SHA512, rsa};
171 cand[3] = {HOSTKEY_RSA_SHA256, rsa};
174 for (
int k = 0; k < 4; k++)
178 size_t l = strnlen(out, cap);
179 snprintf(out + l, cap - l,
"%s%s", l ?
"," :
"", cand[k].name);
197void w_bytes(Writer &w,
const void *src,
size_t n)
199 if (!w.ok || w.len + n > w.cap)
204 memcpy(w.p + w.len, src, n);
208void w_u8(Writer &w, uint8_t v)
213void w_u32(Writer &w, uint32_t v)
215 uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
220void w_namelist(Writer &w,
const char *list)
222 uint32_t n = (uint32_t)strnlen(list, w.cap);
228void w_string(Writer &w,
const uint8_t *data,
size_t n)
230 w_u32(w, (uint32_t)n);
236void w_mpint(Writer &w,
const uint8_t *be,
size_t len)
239 while (off < len && be[off] == 0)
246 bool pad = (be[off] & 0x80u) != 0;
247 w_u32(w, (uint32_t)(len - off) + (pad ? 1u : 0u));
250 w_bytes(w, be + off, len - off);
260static bool namelist_contains(
const uint8_t *list, uint32_t len,
const char *want)
262 size_t wl = strnlen(want, (
size_t)len + 1);
264 for (uint32_t i = 0; i <= len; i++)
266 if (i == len || list[i] ==
',')
268 uint32_t elen = i - start;
269 if (elen == wl && memcmp(list + start, want, wl) == 0)
292static bool negotiate_alg(
const uint8_t *client_list, uint32_t nlen,
const AlgCand<E> *cands,
int n, E *out)
294 for (
int i = 0; i < n; i++)
295 if (cands[i].avail && namelist_contains(client_list, nlen, cands[i].name))
312 memset(s, 0,
sizeof(*s));
327 out[vlen + 1] =
'\n';
341 uint8_t c = data[k++];
351 if (n >= 4 && memcmp(s->
banner_buf,
"SSH-", 4) == 0)
387 Writer w = {payload, cap, 0,
true};
392 w_bytes(w, cookie,
sizeof(cookie));
396 build_kex_list(kexlist,
sizeof(kexlist));
397 build_hostkey_list(hklist,
sizeof(hklist));
398 w_namelist(w, kexlist);
399 w_namelist(w, hklist);
400 w_namelist(w, ALG_CIPHER_LIST);
401 w_namelist(w, ALG_CIPHER_LIST);
402 w_namelist(w, ALG_MAC_LIST);
403 w_namelist(w, ALG_MAC_LIST);
404 w_namelist(w, ALG_COMP);
405 w_namelist(w, ALG_COMP_S2C);
415 if (w.len > SSH_KEXINIT_S_MAX)
417 memcpy(s->
i_s, payload, w.len);
426static bool read_namelist(
const uint8_t *p,
size_t len,
size_t *off,
const uint8_t **list, uint32_t *nlen)
430 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
431 (uint32_t)p[*off + 3];
443static bool negotiate_kex(
const uint8_t *list, uint32_t nlen,
SshKexAlg *out)
447#if DETWS_ENABLE_PQC_KEX
464 return negotiate_alg(list, nlen, kc, nk, out);
469static bool negotiate_hostkey(
const uint8_t *list, uint32_t nlen,
SshHostkeyAlg *out)
471 const bool rsa = hostkey_rsa_available();
489 return negotiate_alg(list, nlen, hc, 4, out);
504 memcpy(s->
i_c, payload, len);
513 if (!read_namelist(payload, len, &off, &list, &nlen))
516 s->
ext_info_c = namelist_contains(list, nlen, EXT_INFO_C);
517 if (!negotiate_kex(list, nlen, &s->
kex_alg))
520 if (!read_namelist(payload, len, &off, &list, &nlen))
522 if (!negotiate_hostkey(list, nlen, &s->
hostkey_alg))
530 if (!read_namelist(payload, len, &off, &list, &nlen))
534 if (!negotiate_alg(list, nlen, cc, 3, &c2s))
536 if (!read_namelist(payload, len, &off, &list, &nlen))
538 if (!negotiate_alg(list, nlen, cc, 3, &s2c) || s2c != c2s)
551 if (!read_namelist(payload, len, &off, &list, &nlen))
553 if (need_mac && !negotiate_alg(list, nlen, mc, 4, &m_c2s))
555 if (!read_namelist(payload, len, &off, &list, &nlen))
557 if (need_mac && (!negotiate_alg(list, nlen, mc, 4, &m_s2c) || m_s2c != m_c2s))
561 if (!read_namelist(payload, len, &off, &list, &nlen) || !namelist_contains(list, nlen, ALG_COMP))
564 if (!read_namelist(payload, len, &off, &list, &nlen))
566#if DETWS_ENABLE_SSH_ZLIB
568 const AlgCand<SshCompAlg> compc[3] = {{
"zlib@openssh.com", SshCompAlg::SSH_COMP_ZLIB_DELAYED,
true},
569 {
"zlib", SshCompAlg::SSH_COMP_ZLIB,
true},
570 {
"none", SshCompAlg::SSH_COMP_NONE,
true}};
572 if (!negotiate_alg(list, nlen, compc, 3, &comp))
574 ssh_comp_set_s2c(i, comp);
577 if (!namelist_contains(list, nlen, ALG_COMP))
588 Writer w = {out, cap, 0,
true};
591 w_namelist(w,
"server-sig-algs");
598 const char *siglist = s_sshtr.
prefer_rsa ?
"rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256,ssh-ed25519"
599 :
"ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256";
600 w_namelist(w, siglist);
614 uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
619static void hash_string(
SshSha256Ctx *ctx,
const uint8_t *data,
size_t len)
621 hash_u32(ctx, (uint32_t)len);
627static void hash_mpint(
SshSha256Ctx *ctx,
const uint8_t *be,
size_t len)
630 while (off < len && be[off] == 0)
638 bool pad = (be[off] & 0x80u) != 0;
639 uint32_t mlen = (uint32_t)(len - off) + (pad ? 1u : 0u);
655static int compute_exchange_hash(uint8_t i,
bool pub_is_string,
const uint8_t *cpub,
size_t cpub_len,
656 const uint8_t *spub,
size_t spub_len,
const uint8_t *k_be,
size_t k_len,
667 hash_string(&ctx, (
const uint8_t *)s->
v_c, s->
v_c_len);
671 hash_string(&ctx, ks, ks_len);
674 hash_string(&ctx, cpub, cpub_len);
675 hash_string(&ctx, spub, spub_len);
679 hash_mpint(&ctx, cpub, cpub_len);
680 hash_mpint(&ctx, spub, spub_len);
683 hash_string(&ctx, k_be, k_len);
685 hash_mpint(&ctx, k_be, k_len);
690int 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,
693 return compute_exchange_hash(i,
false, e_be, 256, f_be, 256, k_be, 256, ks, ks_len, out,
false);
705 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
706 (uint32_t)payload[4];
707 if ((
size_t)5 + n > len)
710 const uint8_t *m = payload + 5;
712 while (off < n && m[off] == 0)
714 size_t vlen = n - off;
718 memset(e_be, 0, 256);
719 memcpy(e_be + (256 - vlen), m + off, vlen);
724 uint8_t *out,
size_t *out_len,
size_t cap)
726 Writer w = {out, cap, 0,
true};
728 w_string(w, ks, ks_len);
729 w_mpint(w, f_be, 256);
732 uint32_t inner = 4 + 12 + 4 + (uint32_t)sig_len;
734 w_string(w, (
const uint8_t *)
"rsa-sha2-256", 12);
735 w_string(w, sig, sig_len);
745static int parse_ecdh_init(
const uint8_t *payload,
size_t len, uint8_t qc[32])
749 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
750 (uint32_t)payload[4];
751 if (n != 32 || (
size_t)5 + n > len)
753 memcpy(qc, payload + 5, 32);
759static int parse_ecdh_init_p256(
const uint8_t *payload,
size_t len, uint8_t qc[SSH_ECDSA_P256_PUB_LEN])
763 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
764 (uint32_t)payload[4];
765 if (n != SSH_ECDSA_P256_PUB_LEN || (
size_t)5 + n > len)
767 memcpy(qc, payload + 5, SSH_ECDSA_P256_PUB_LEN);
775static int encode_hostkey(uint8_t i, uint8_t *ks,
size_t *ks_len,
size_t cap)
779 Writer w = {ks, cap, 0,
true};
780 w_string(w, (
const uint8_t *)HOSTKEY_ED,
sizeof(HOSTKEY_ED) - 1);
781 w_string(w, s_sshtr.
ed_pub, 32);
789 Writer w = {ks, cap, 0,
true};
790 w_string(w, (
const uint8_t *)HOSTKEY_ECDSA,
sizeof(HOSTKEY_ECDSA) - 1);
791 w_string(w, (
const uint8_t *)
"nistp256", 8);
792 w_string(w, s_sshtr.
ecdsa_pub, SSH_ECDSA_P256_PUB_LEN);
803static int sign_hash(uint8_t i,
const uint8_t H[
SSH_SHA256_DIGEST_LEN], uint8_t *sig,
size_t *sig_len,
size_t sig_cap,
804 const char **sig_name)
812 *sig_name = HOSTKEY_ED;
817 uint8_t raw[SSH_ECDSA_P256_SIG_LEN];
821 Writer w = {sig, sig_cap, 0,
true};
822 w_mpint(w, raw, SSH_ECDSA_P256_COORD_LEN);
823 w_mpint(w, raw + SSH_ECDSA_P256_COORD_LEN, SSH_ECDSA_P256_COORD_LEN);
827 *sig_name = HOSTKEY_ECDSA;
838 *sig_name = sha512 ? HOSTKEY_RSA_SHA512 : HOSTKEY_RSA_SHA256;
844static int build_kex_reply(uint8_t i,
const uint8_t *ks,
size_t ks_len,
const uint8_t *spub,
size_t spub_len,
845 const char *sig_name,
const uint8_t *sig,
size_t sig_len, uint8_t *out,
size_t *out_len,
848 Writer w = {out, cap, 0,
true};
850 w_string(w, ks, ks_len);
852 w_mpint(w, spub, spub_len);
854 w_string(w, spub, spub_len);
855 uint32_t nl = (uint32_t)strnlen(sig_name, w.cap);
856 w_u32(w, 4 + nl + 4 + (uint32_t)sig_len);
857 w_string(w, (
const uint8_t *)sig_name, nl);
858 w_string(w, sig, sig_len);
871#if DETWS_ENABLE_PQC_KEX
886 uint8_t qtmp[SSH_ECDSA_P256_PUB_LEN];
887 for (
int t = 0; t < 8; t++)
898#if DETWS_ENABLE_PQC_KEX
903static int hybrid_mlkem_x25519(uint8_t i,
const uint8_t *payload,
size_t len, uint8_t s_reply[MLKEM768_CT_BYTES + 32],
908 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
909 (uint32_t)payload[4];
910 if (n != MLKEM768_EK_BYTES + 32 || (
size_t)5 + n > len)
912 const uint8_t *ek = payload + 5;
913 const uint8_t *qc = payload + 5 + MLKEM768_EK_BYTES;
918 bool ok = mlkem768_encaps(ek, m, s_reply, k_pq);
919 ssh_wipe(m,
sizeof(m));
926 for (
int b = 0; b < 32; b++)
930 ssh_wipe(k_pq,
sizeof(k_pq));
931 ssh_wipe(k_cl,
sizeof(k_cl));
934 memcpy(s_reply + MLKEM768_CT_BYTES,
ssh_sess[i].ecdh_pk, 32);
941 ssh_wipe(k_pq,
sizeof(k_pq));
942 ssh_wipe(k_cl,
sizeof(k_cl));
947int ssh_kexdh_handle(uint8_t i,
const uint8_t *payload,
size_t len, uint8_t *reply_out,
size_t *reply_len,
size_t cap)
958 memset(k_be, 0,
sizeof(k_be));
961 const uint8_t *cpub_p = cpub;
962 const uint8_t *spub_p = spub;
963 size_t cpub_len = 256;
964 size_t spub_len = 256;
965 const uint8_t *k_hash = k_be;
966 size_t k_hash_len = 256;
967 bool pub_is_string =
false;
968 bool k_is_string =
false;
969#if DETWS_ENABLE_PQC_KEX
970 uint8_t s_reply[MLKEM768_CT_BYTES + 32];
978 if (parse_ecdh_init(payload, len, qc) != 0)
983 for (
int b = 0; b < 32; b++)
987 ssh_wipe(kk,
sizeof(kk));
990 memcpy(k_be + (256 - 32), kk, 32);
991 memcpy(cpub, qc, 32);
993 cpub_len = spub_len = 32;
994 pub_is_string =
true;
995 ssh_wipe(kk,
sizeof(kk));
997#if DETWS_ENABLE_PQC_KEX
1001 if (hybrid_mlkem_x25519(i, payload, len, s_reply, k_be + (256 - 32)) != 0)
1003 cpub_p = payload + 5;
1004 cpub_len = MLKEM768_EK_BYTES + 32;
1006 spub_len = MLKEM768_CT_BYTES + 32;
1007 k_hash = k_be + (256 - 32);
1009 pub_is_string =
true;
1016 uint8_t qc[SSH_ECDSA_P256_PUB_LEN];
1017 if (parse_ecdh_init_p256(payload, len, qc) != 0)
1019 uint8_t qs[SSH_ECDSA_P256_PUB_LEN];
1020 uint8_t kk[SSH_ECDSA_P256_COORD_LEN];
1025 memcpy(k_be + (256 - SSH_ECDSA_P256_COORD_LEN), kk, SSH_ECDSA_P256_COORD_LEN);
1026 memcpy(cpub, qc, SSH_ECDSA_P256_PUB_LEN);
1027 memcpy(spub, qs, SSH_ECDSA_P256_PUB_LEN);
1028 cpub_len = SSH_ECDSA_P256_PUB_LEN;
1029 spub_len = SSH_ECDSA_P256_PUB_LEN;
1030 pub_is_string =
true;
1031 ssh_wipe(kk,
sizeof(kk));
1046 ssh_wipe(&K,
sizeof(K));
1047 memcpy(cpub, e_be, 256);
1054 if (encode_hostkey(i, ks, &ks_len,
sizeof(ks)) != 0)
1056 ssh_wipe(k_be,
sizeof(k_be));
1062 compute_exchange_hash(i, pub_is_string, cpub_p, cpub_len, spub_p, spub_len, k_hash, k_hash_len, ks, ks_len, H,
1073 const char *sig_name =
nullptr;
1074 if (sign_hash(i, H, sig, &sig_len,
sizeof(sig), &sig_name) != 0)
1076 ssh_wipe(k_be,
sizeof(k_be));
1081 if (build_kex_reply(i, ks, ks_len, spub_p, spub_len, sig_name, sig, sig_len, reply_out, reply_len, cap) != 0)
1083 ssh_wipe(k_be,
sizeof(k_be));
1087 ssh_wipe(k_be,
sizeof(k_be));
1099#if DETWS_ENABLE_SSH_ZLIB
1101 ssh_comp_on_newkeys(i);
1127bool ssh_rekey_due(uint32_t seq_send, uint32_t seq_recv, uint32_t elapsed_ms, uint32_t pkt_threshold,
1128 uint32_t time_threshold_ms)
1130 if (seq_send >= pkt_threshold || seq_recv >= pkt_threshold)
1132 if (time_threshold_ms && elapsed_ms >= time_threshold_ms)
#define SSH_KEXINIT_MAX
Max stored size of the CLIENT KEXINIT payload (I_C, for the exchange hash).
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
#define SSH_REKEY_PACKET_THRESHOLD
Re-key when either packet sequence number reaches this value.
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
ML-KEM-768 encapsulation (FIPS 203), responder side only.
void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp)
Compute out = base^exp mod group14_p.
void bn_to_bytes(uint8_t bytes[256], const SshBigNum *in)
Write a SshBigNum as a 256-byte big-endian array.
int bn_dh_validate(const SshBigNum *v)
Validate a received DH public value.
void bn_from_bytes(SshBigNum *out, const uint8_t *bytes, size_t len)
Read a big-endian byte array of len bytes into a SshBigNum.
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
void ssh_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
void ssh_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
void ssh_dh_derive_keys_sid(uint8_t i, const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN], const uint8_t session_id[SSH_SHA256_DIGEST_LEN], uint8_t cipher_alg, uint8_t mac_alg, bool k_is_string)
Derive session keys with an explicit session id (RFC 4253 §7.2).
void ssh_rng_fill(uint8_t *buf, size_t len)
Fill len bytes of buf with cryptographically random data.
int ssh_dh_generate(uint8_t i)
Generate the server ephemeral DH key pair for connection slot i.
DH-group14-SHA256 key exchange (RFC 4253 §8 + RFC 8268).
bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
P-256 ECDH: the shared-secret X coordinate of d * Q_peer (RFC 5656 §4 / RFC 5903).
bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen, const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Sign mlen bytes of msg with a P-256 private key (ECDSA, SHA-256).
bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Derive the uncompressed public point Q = d*G from a P-256 private scalar.
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
void ssh_ed25519_sign(uint8_t sig[64], const uint8_t *msg, size_t mlen, const uint8_t seed[32])
void ssh_ed25519_pubkey(uint8_t pub[32], const uint8_t seed[32])
Ed25519 signatures (RFC 8032) for ssh-ed25519 host keys + client auth.
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)
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
@ SSH_MAC_HMAC_SHA256_ETM
hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
@ SSH_MAC_HMAC_SHA512
hmac-sha2-512 (encrypt-and-MAC)
@ SSH_MAC_HMAC_SHA512_ETM
hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
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_KEXDH_REPLY
#define SSH_MSG_KEXDH_INIT
int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t sig[SSH_RSA_SIG_BYTES])
Sign msg using the RSA host key (PKCS#1 v1.5, rsa-sha2-256/512).
SshRsaPubKey ssh_host_pubkey
Static host public key (BSS). Set by ssh_rsa_load_pubkey().
int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
Serialize the RSA public host key into an SSH "ssh-rsa" key blob.
RSA-SHA2-256/512 host-key signing and public-key serialization.
SshRsaHash
Hash algorithm selecting the RSA signature scheme (RFC 8332).
#define SSH_RSA_SIG_BYTES
PKCS#1 v1.5 signature size for RSA-2048 in bytes.
#define SSH_RSA_PUBKEY_BLOB_MAX
Maximum byte length of the serialized RSA public key blob.
void ssh_sha256_init(SshSha256Ctx *ctx)
Initialize a streaming SHA-256 context.
void ssh_sha256_update(SshSha256Ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
void ssh_sha256_final(SshSha256Ctx *ctx, uint8_t digest[SSH_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
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).
bool ssh_hostkey_ed25519_available(void)
True if an ssh-ed25519 host key has been installed.
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.
void ssh_hostkey_ed25519_set(const uint8_t seed[32])
Install an ssh-ed25519 host key from its 32-byte seed (RFC 8032 private key).
void ssh_hostkey_ecdsa_set(const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
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 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_hostkey_ecdsa_available(void)
True if an ecdsa-sha2-nistp256 host key has been installed.
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).
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.
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[SSH_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 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_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).
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
bool enc_out
True once we have sent our NEWKEYS (outbound cipher/MAC active).
uint32_t seq_no_recv
Incoming sequence number (incremented per packet).
bool enc_in
True once we have received the peer's NEWKEYS (inbound cipher/MAC active).
bool kex_active
True while KEX is in progress (no user data).
uint32_t seq_no_send
Outgoing sequence number (incremented per packet).
bool loaded
True after ssh_rsa_load_pubkey() succeeds.
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.
uint8_t session_id[SSH_SHA256_DIGEST_LEN]
H from the first KEX (RFC 4253 §7.2).
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).
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
detws_millis() when the last KEX completed (server-initiated re-key timer).
uint8_t i_s[SSH_KEXINIT_S_MAX]
Server KEXINIT payload (for H).
uint8_t ecdh_sk[32]
Server X25519 ephemeral private (curve25519 KEX only; wiped after).
uint8_t cipher_alg
SSH_CIPHER_* negotiated in KEXINIT (0 = aes256-ctr).
uint16_t v_c_len
Length of v_c.
Streaming SHA-256 context.
uint8_t ecdsa_priv[SSH_ECDSA_P256_PRIV_LEN]
P-256 host private scalar d.
uint8_t ecdsa_pub[SSH_ECDSA_P256_PUB_LEN]
P-256 host public point (0x04||X||Y).