24#if PC_ENABLE_SSH_SNTRUP761
33#ifdef PC_SSH_KEX_BENCH
37SshKexBenchCtx pc_ssh_kex_bench = {0, 0, 0};
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";
53static const char *
const KEX_ECDH_NISTP256 =
"ecdh-sha2-nistp256";
55static const char *
const KEX_MLKEM768 =
"mlkem768x25519-sha256";
57#if PC_ENABLE_SSH_SNTRUP761
58static const char *
const KEX_SNTRUP761 =
"sntrup761x25519-sha512@openssh.com";
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";
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";
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";
79static const char *
const ALG_COMP_ZLIB =
"zlib@openssh.com,zlib,none";
81static const char *
const ALG_COMP_ZLIB =
"none";
84static const char *
const EXT_INFO_C =
"ext-info-c";
113 memcpy(s_sshtr.
ed_seed, seed, 32);
135static bool hostkey_rsa_available(
void)
143static void build_kex_list(
char *out,
size_t cap)
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;
155 pc_sb sb_mlkem = {out + n, cap - n, 0,
true};
160#if PC_ENABLE_SSH_SNTRUP761
161 pc_sb sb_sntrup = {out + n, cap - n, 0,
true};
168 pc_sb sb157 = {out + n, cap - n, 0,
true};
184 pc_sb sb159 = {out + n, cap - n, 0,
true};
199static void build_hostkey_list(
char *out,
size_t cap)
203 const bool rsa = hostkey_rsa_available();
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};
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};
227 for (
int k = 0; k < 4; k++)
233 size_t l = strnlen(out, cap);
234 pc_sb sb194 = {out + l, cap - l, 0,
true};
258void w_bytes(Writer &w,
const void *src,
size_t n)
260 if (!w.ok || w.len + n > w.cap)
265 memcpy(w.p + w.len, src, n);
269void w_u8(Writer &w, uint8_t v)
274void w_u32(Writer &w, uint32_t v)
276 uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
281void w_namelist(Writer &w,
const char *list)
283 uint32_t n = (uint32_t)strnlen(list, w.cap);
289void w_string(Writer &w,
const uint8_t *data,
size_t n)
291 w_u32(w, (uint32_t)n);
297void w_mpint(Writer &w,
const uint8_t *be,
size_t len)
300 while (off < len && be[off] == 0)
309 bool pad = (be[off] & 0x80u) != 0;
310 w_u32(w, (uint32_t)(len - off) + (pad ? 1u : 0u));
315 w_bytes(w, be + off, len - off);
325static bool namelist_contains(
const uint8_t *list, uint32_t len,
const char *want)
327 size_t wl = strnlen(want, (
size_t)len + 1);
329 for (uint32_t i = 0; i <= len; i++)
331 if (i == len || list[i] ==
',')
333 uint32_t elen = i - start;
334 if (elen == wl && memcmp(list + start, want, wl) == 0)
357template <
typename E>
static int cand_match(
const uint8_t *tok, uint32_t tlen,
const AlgCand<E> *cands,
int n)
359 for (
int c = 0; c < n; c++)
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)
376static bool negotiate_alg(
const uint8_t *client_list, uint32_t nlen,
const AlgCand<E> *cands,
int n, E *out)
379 for (uint32_t i = 0; i <= nlen; i++)
381 if (i == nlen || client_list[i] ==
',')
383 int c = cand_match(client_list + start, i - start, cands, n);
406 memset(s, 0,
sizeof(*s));
423 out[vlen + 1] =
'\n';
439 uint8_t c = data[k++];
451 if (n >= 4 && memcmp(s->
banner_buf,
"SSH-", 4) == 0)
493 Writer w = {payload, cap, 0,
true};
498 w_bytes(w, cookie,
sizeof(cookie));
504 build_kex_list(kexlist,
sizeof(kexlist));
505 build_hostkey_list(hklist,
sizeof(hklist));
506 w_namelist(w, kexlist);
507 w_namelist(w, hklist);
508 w_namelist(w, ALG_CIPHER_LIST);
509 w_namelist(w, ALG_CIPHER_LIST);
510 w_namelist(w, ALG_MAC_LIST);
511 w_namelist(w, ALG_MAC_LIST);
512 w_namelist(w, ALG_COMP_ZLIB);
513 w_namelist(w, ALG_COMP_ZLIB);
530 memcpy(s->
i_s, payload, w.len);
539static bool read_namelist(
const uint8_t *p,
size_t len,
size_t *off,
const uint8_t **list, uint32_t *nlen)
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];
560static bool negotiate_kex(
const uint8_t *list, uint32_t nlen,
SshKexAlg *out)
567#if PC_ENABLE_SSH_SNTRUP761
584 return negotiate_alg(list, nlen, kc, nk, out);
589static bool negotiate_hostkey(
const uint8_t *list, uint32_t nlen,
SshHostkeyAlg *out)
591 const bool rsa = hostkey_rsa_available();
609 return negotiate_alg(list, nlen, hc, 4, out);
630 memcpy(s->
i_c, payload, len);
639 if (!read_namelist(payload, len, &off, &list, &nlen))
644 s->
ext_info_c = namelist_contains(list, nlen, EXT_INFO_C);
645 if (!negotiate_kex(list, nlen, &s->
kex_alg))
650 if (!read_namelist(payload, len, &off, &list, &nlen))
654 if (!negotiate_hostkey(list, nlen, &s->
hostkey_alg))
664 if (!read_namelist(payload, len, &off, &list, &nlen))
670 if (!negotiate_alg(list, nlen, cc, 3, &c2s))
674 if (!read_namelist(payload, len, &off, &list, &nlen))
678 if (!negotiate_alg(list, nlen, cc, 3, &s2c) || s2c != c2s)
693 if (!read_namelist(payload, len, &off, &list, &nlen))
697 if (need_mac && !negotiate_alg(list, nlen, mc, 4, &m_c2s))
701 if (!read_namelist(payload, len, &off, &list, &nlen))
705 if (need_mac && (!negotiate_alg(list, nlen, mc, 4, &m_s2c) || m_s2c != m_c2s))
712#if PC_ENABLE_SSH_ZLIB
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}};
718 if (!read_namelist(payload, len, &off, &list, &nlen) || !negotiate_alg(list, nlen, compc, 3, &comp))
722 ssh_comp_set_c2s(i, comp);
723 if (!read_namelist(payload, len, &off, &list, &nlen) || !negotiate_alg(list, nlen, compc, 3, &comp))
727 ssh_comp_set_s2c(i, comp);
731 if (!read_namelist(payload, len, &off, &list, &nlen) || !namelist_contains(list, nlen, ALG_COMP))
735 if (!read_namelist(payload, len, &off, &list, &nlen) || !namelist_contains(list, nlen, ALG_COMP))
748 Writer w = {out, cap, 0,
true};
751 w_namelist(w,
"server-sig-algs");
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);
775static void hash_u32(
SshKexHash *h, uint32_t v)
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);
782static void hash_string(
SshKexHash *h,
const uint8_t *data,
size_t len)
784 hash_u32(h, (uint32_t)len);
785 ssh_kexhash_update(h, data, len);
790static void hash_mpint(
SshKexHash *h,
const uint8_t *be,
size_t len)
793 while (off < len && be[off] == 0)
803 bool pad = (be[off] & 0x80u) != 0;
804 uint32_t mlen = (uint32_t)(len - off) + (pad ? 1u : 0u);
809 ssh_kexhash_update(h, &zero, 1);
811 ssh_kexhash_update(h, be + off, len - off);
815static inline bool kex_is_sha512(
SshKexAlg a)
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,
831 bool k_is_string,
bool is512)
842 ssh_kexhash_init(&h, is512);
843 hash_string(&h, (
const uint8_t *)s->
v_c, s->
v_c_len);
847 hash_string(&h, ks, ks_len);
850 hash_string(&h, cpub, cpub_len);
851 hash_string(&h, spub, spub_len);
855 hash_mpint(&h, cpub, cpub_len);
856 hash_mpint(&h, spub, spub_len);
860 hash_string(&h, k_be, k_len);
864 hash_mpint(&h, k_be, k_len);
866 *out_len = ssh_kexhash_final(&h, out);
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,
874 return compute_exchange_hash(i,
false, e_be, 256, f_be, 256, k_be, 256, ks, ks_len, out, &out_len,
false,
false);
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)
895 const uint8_t *m = payload + 5;
897 while (off < n && m[off] == 0)
901 size_t vlen = n - off;
907 memset(e_be, 0, 256);
908 memcpy(e_be + (256 - vlen), m + off, vlen);
913 uint8_t *out,
size_t *out_len,
size_t cap)
915 Writer w = {out, cap, 0,
true};
917 w_string(w, ks, ks_len);
918 w_mpint(w, f_be, 256);
921 uint32_t inner = 4 + 12 + 4 + (uint32_t)sig_len;
923 w_string(w, (
const uint8_t *)
"rsa-sha2-256", 12);
924 w_string(w, sig, sig_len);
936static int parse_ecdh_init(
const uint8_t *payload,
size_t len, uint8_t qc[32])
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)
948 memcpy(qc, payload + 5, 32);
954static int parse_ecdh_init_p256(
const uint8_t *payload,
size_t len, uint8_t qc[
PC_ECDSA_P256_PUB_LEN])
960 uint32_t n = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) |
961 (uint32_t)payload[4];
974static int encode_hostkey(uint8_t i, uint8_t *ks,
size_t *ks_len,
size_t cap)
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);
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);
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)
1017 *sig_name = HOSTKEY_ED;
1028 Writer w = {sig, sig_cap, 0,
true};
1036 *sig_name = HOSTKEY_ECDSA;
1049 *sig_name = sha512 ? HOSTKEY_RSA_SHA512 : HOSTKEY_RSA_SHA256;
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,
1059 Writer w = {out, cap, 0,
true};
1061 w_string(w, ks, ks_len);
1064 w_mpint(w, spub, spub_len);
1068 w_string(w, spub, spub_len);
1070 uint32_t nl = (uint32_t)strnlen(sig_name, w.cap);
1071 w_u32(w, 4 + nl + 4 + (uint32_t)sig_len);
1072 w_string(w, (
const uint8_t *)sig_name, nl);
1073 w_string(w, sig, sig_len);
1090#if PC_ENABLE_PQC_KEX
1097#ifdef PC_SSH_KEX_BENCH
1098 int64_t kexgen_t0 = esp_timer_get_time();
1102#ifdef PC_SSH_KEX_BENCH
1103 pc_ssh_kex_bench.last_kexgen_us = (
long long)(esp_timer_get_time() - kexgen_t0);
1113 for (
int t = 0; t < 8; t++)
1126#if PC_ENABLE_PQC_KEX
1131static int hybrid_mlkem_x25519(uint8_t i,
const uint8_t *payload,
size_t len, uint8_t s_reply[MLKEM768_CT_BYTES + 32],
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)
1144 const uint8_t *ek = payload + 5;
1145 const uint8_t *qc = payload + 5 + MLKEM768_EK_BYTES;
1150 bool ok = pc_mlkem768_encaps(ek, m, s_reply, k_pq);
1151 pc_secure_wipe(m,
sizeof(m));
1160 for (
int b = 0; b < 32; b++)
1166 pc_secure_wipe(k_pq,
sizeof(k_pq));
1167 pc_secure_wipe(k_cl,
sizeof(k_cl));
1170 memcpy(s_reply + MLKEM768_CT_BYTES,
ssh_sess[i].ecdh_pk, 32);
1177 pc_secure_wipe(k_pq,
sizeof(k_pq));
1178 pc_secure_wipe(k_cl,
sizeof(k_cl));
1183#if PC_ENABLE_SSH_SNTRUP761
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])
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)
1201 const uint8_t *pk = payload + 5;
1202 const uint8_t *qc = payload + 5 + PC_SNTRUP761_PK_BYTES;
1204 uint8_t k_pq[PC_SNTRUP761_SS_BYTES];
1205 pc_sntrup761_enc(pk, s_reply, k_pq);
1210 for (
int b = 0; b < 32; b++)
1216 pc_secure_wipe(k_pq,
sizeof(k_pq));
1217 pc_secure_wipe(k_cl,
sizeof(k_cl));
1220 memcpy(s_reply + PC_SNTRUP761_CT_BYTES,
ssh_sess[i].ecdh_pk, 32);
1227 pc_secure_wipe(k_pq,
sizeof(k_pq));
1228 pc_secure_wipe(k_cl,
sizeof(k_cl));
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)
1240#ifdef PC_SSH_KEX_BENCH
1241 int64_t kexreply_t0 = esp_timer_get_time();
1249 memset(k_be, 0,
sizeof(k_be));
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];
1269 if (parse_ecdh_init(payload, len, qc) != 0)
1276 for (
int b = 0; b < 32; b++)
1282 pc_secure_wipe(kk,
sizeof(kk));
1285 memcpy(k_be + (256 - 32), kk, 32);
1286 memcpy(cpub, qc, 32);
1288 cpub_len = spub_len = 32;
1289 pub_is_string =
true;
1290 pc_secure_wipe(kk,
sizeof(kk));
1292#if PC_ENABLE_PQC_KEX
1296 if (hybrid_mlkem_x25519(i, payload, len, s_reply, k_be + (256 - 32)) != 0)
1300 cpub_p = payload + 5;
1301 cpub_len = MLKEM768_EK_BYTES + 32;
1303 spub_len = MLKEM768_CT_BYTES + 32;
1304 k_hash = k_be + (256 - 32);
1306 pub_is_string =
true;
1310#if PC_ENABLE_SSH_SNTRUP761
1314 if (hybrid_sntrup761_x25519(i, payload, len, s_reply, k_be + (256 - 64)) != 0)
1318 cpub_p = payload + 5;
1319 cpub_len = PC_SNTRUP761_PK_BYTES + 32;
1321 spub_len = PC_SNTRUP761_CT_BYTES + 32;
1322 k_hash = k_be + (256 - 64);
1324 pub_is_string =
true;
1332 if (parse_ecdh_init_p256(payload, len, qc) != 0)
1349 pub_is_string =
true;
1350 pc_secure_wipe(kk,
sizeof(kk));
1369 pc_secure_wipe(&K,
sizeof(K));
1370 memcpy(cpub, e_be, 256);
1377 if (encode_hostkey(i, ks, &ks_len,
sizeof(ks)) != 0)
1379 pc_secure_wipe(k_be,
sizeof(k_be));
1384 const bool is512 = kex_is_sha512(s->
kex_alg);
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);
1399 const char *sig_name =
nullptr;
1400 if (sign_hash(i, H, h_len, sig, &sig_len,
sizeof(sig), &sig_name) != 0)
1402 pc_secure_wipe(k_be,
sizeof(k_be));
1407 if (build_kex_reply(i, ks, ks_len, spub_p, spub_len, sig_name, sig, sig_len, reply_out, reply_len, cap) != 0)
1409 pc_secure_wipe(k_be,
sizeof(k_be));
1414 pc_secure_wipe(k_be,
sizeof(k_be));
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++;
1432#if PC_ENABLE_SSH_ZLIB
1434 ssh_comp_on_newkeys(i);
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)
1467 if (seq_send >= pkt_threshold || seq_recv >= pkt_threshold)
1471 if (time_threshold_ms && elapsed_ms >= time_threshold_ms)
void bn_to_bytes(uint8_t bytes[256], const pc_bignum *in)
Write a pc_bignum as a 256-byte big-endian array.
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.
int bn_dh_validate(const pc_bignum *v)
Validate a received DH public value.
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
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).
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).
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.
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).
#define PC_ECDSA_P256_SIG_LEN
Raw ECDSA signature length: r || s (32 + 32, big-endian).
#define PC_ECDSA_P256_PUB_LEN
P-256 uncompressed public point length: 0x04 || X || Y.
#define PC_ECDSA_P256_PRIV_LEN
P-256 private key (scalar d) length.
void pc_ed25519_pubkey(uint8_t pub[32], const uint8_t seed[32])
void pc_ed25519_sign(uint8_t sig[64], const uint8_t *msg, size_t mlen, const uint8_t seed[32])
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).
@ 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.
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).
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...
void pc_sha256_update(pc_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
void pc_sha512_update(pc_sha512_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
PC_CRYPTO_HOT void pc_sha512_init(pc_sha512_ctx *ctx)
Initialize a streaming SHA-512 context (ctx must not be NULL).
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...
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.
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).
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).
#define SSH_KEXHASH_MAX_LEN
longest exchange-hash / session_id (SHA-512)
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
SshRsaPubKey ssh_host_pubkey
Static host public key (BSS). Set by pc_ssh_rsa_load_pubkey().
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).
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.
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).
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.
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.
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.
A key-exchange digest bound to one of the SSH KEX hashes (SHA-256 or SHA-512).
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 pc_ssh_rsa_load_pubkey() succeeds.
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 ecdsa_priv[PC_ECDSA_P256_PRIV_LEN]
P-256 host private scalar d.
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.
Bump-append target; ok latches false once an append would overflow cap.
Streaming SHA-256 context.
Streaming SHA-512 context.