17#if PC_ENABLE_SSH_CLIENT
35#if PC_ENABLE_SSH_SNTRUP761
38#if PC_ENABLE_PQC_KEX || PC_ENABLE_SSH_SNTRUP761
52static const char CLIENT_BANNER[] =
"SSH-2.0-PC_client_1.0";
62static const char NAME_ED25519[] =
"ssh-ed25519";
65static const char *
const KEX_NAMES[] = {
67 "mlkem768x25519-sha256",
69#if PC_ENABLE_SSH_SNTRUP761
70 "sntrup761x25519-sha512@openssh.com",
73 "curve25519-sha256@libssh.org",
75 "diffie-hellman-group14-sha256"};
76static const char *
const HOSTKEY_NAMES[] = {
"ssh-ed25519",
"ecdsa-sha2-nistp256",
"rsa-sha2-512",
"rsa-sha2-256"};
77static const char *
const CIPHER_NAMES[] = {
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes256-ctr"};
78static const char *
const MAC_NAMES[] = {
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-256",
79 "hmac-sha2-512-etm@openssh.com",
"hmac-sha2-512"};
82enum class CliKex : uint8_t
90#if PC_ENABLE_SSH_SNTRUP761
95static const CliKex KEX_OF[] = {
97 CliKex::MLKEM768_X25519,
99#if PC_ENABLE_SSH_SNTRUP761
100 CliKex::SNTRUP761_X25519,
102 CliKex::CURVE25519, CliKex::CURVE25519, CliKex::ECDH_P256, CliKex::DH_GROUP14};
105static inline bool cli_kex_is_sha512(CliKex k)
107#if PC_ENABLE_SSH_SNTRUP761
108 return k == CliKex::SNTRUP761_X25519;
115enum class CliHostkey : uint8_t
123#define SSH_CLI_SLOT 0
139void w_u8(Wr *w, uint8_t v)
141 if (w->off + 1 > w->cap)
146 w->buf[w->off++] = v;
148void w_u32(Wr *w, uint32_t v)
150 if (w->off + 4 > w->cap)
155 w->buf[w->off++] = (uint8_t)(v >> 24);
156 w->buf[w->off++] = (uint8_t)(v >> 16);
157 w->buf[w->off++] = (uint8_t)(v >> 8);
158 w->buf[w->off++] = (uint8_t)v;
160void w_bytes(Wr *w,
const uint8_t *d,
size_t n)
162 if (w->off + n > w->cap)
167 memcpy(w->buf + w->off, d, n);
170void w_string(Wr *w,
const void *d,
size_t n)
172 w_u32(w, (uint32_t)n);
173 w_bytes(w, (
const uint8_t *)d, n);
175void w_cstr(Wr *w,
const char *s)
177 w_string(w, s, strnlen(s, w->cap));
190 if (r->off + 1 > r->len)
195 return r->buf[r->off++];
199 if (r->off + 4 > r->len)
204 uint32_t v = ((uint32_t)r->buf[r->off] << 24) | ((uint32_t)r->buf[r->off + 1] << 16) |
205 ((uint32_t)r->buf[r->off + 2] << 8) | (uint32_t)r->buf[r->off + 3];
210const uint8_t *r_string(Rd *r, uint32_t *n)
212 uint32_t l = r_u32(r);
213 if (!r->ok || r->off + l > r->len)
219 const uint8_t *p = r->buf + r->off;
226bool namelist_has(
const uint8_t *list, uint32_t len,
const char *want)
228 size_t wl = strnlen(want, (
size_t)len + 1);
230 for (uint32_t i = 0; i <= len; i++)
232 if (i == len || list[i] ==
',')
234 if (i - start == wl && memcmp(list + start, want, wl) == 0)
245void hash_string(
SshKexHash *c,
const uint8_t *d,
size_t n)
247 uint8_t l[4] = {(uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n};
248 ssh_kexhash_update(c, l, 4);
249 ssh_kexhash_update(c, d, n);
252void hash_mpint(
SshKexHash *c,
const uint8_t *be,
size_t n)
255 while (i < n && be[i] == 0)
260 bool pad = (mlen > 0 && (be[i] & 0x80) != 0);
261 uint8_t l4[4] = {0, 0, 0, (uint8_t)(mlen + (pad ? 1 : 0))};
262 l4[2] = (uint8_t)((mlen + (pad ? 1 : 0)) >> 8);
263 ssh_kexhash_update(c, l4, 4);
267 ssh_kexhash_update(c, &z, 1);
271 ssh_kexhash_update(c, be + i, mlen);
280enum class CliPhase : uint8_t
314 pc_ssh_tunnel_cfg cfg;
316 pc_ssh_tunnel_state state;
319 uint32_t deadline_ms;
326 uint8_t kex_priv[32];
329#if PC_ENABLE_PQC_KEX || PC_ENABLE_SSH_SNTRUP761
336 uint8_t mlkem_dk[MLKEM768_DK_BYTES];
338#if PC_ENABLE_SSH_SNTRUP761
339 uint8_t sntrup_sk[PC_SNTRUP761_SK_BYTES];
355 uint8_t session_id_len;
359 uint32_t next_chan_id;
364static SshClientCtx s_cli;
367static CliChannel *chan_by_local(uint32_t local_id)
371 if (s_cli.chan[i].used && s_cli.chan[i].local_id == local_id)
373 return &s_cli.chan[i];
380static CliChannel *chan_alloc(
void)
384 if (!s_cli.chan[i].used)
386 return &s_cli.chan[i];
399static bool cli_send(
const uint8_t *payload,
size_t len)
402 if (
ssh_pkt_send(SSH_CLI_SLOT, payload, len, s_cli.wire, &wlen,
sizeof(s_cli.wire)) != 0)
411static const pc_field LOG_TUNNEL_NEGOTIATED[] = {{
PC_FK_LIT, 0, 27,
"ssh-tunnel: negotiated kex="},
418static const pc_field LOG_TUNNEL_FWD_OPEN[] = {{
PC_FK_LIT, 0, 49,
"ssh-tunnel: forwarded-tcpip open, local connect(:"},
423static const pc_field LOG_TUNNEL_UP[] = {
426static void cli_fail(
const char *why)
429 s_cli.phase = CliPhase::FAILED;
430 s_cli.state = pc_ssh_tunnel_state::PC_TUN_FAILED;
433 if (s_cli.chan[i].used && s_cli.chan[i].local_cid >= 0)
443 ssh_keymat_wipe(SSH_CLI_SLOT);
444 pc_secure_wipe(s_cli.kex_priv,
sizeof(s_cli.kex_priv));
452static void w_namelist(Wr *w,
const char *
const *names,
size_t n)
456 for (
size_t i = 0; i < n; i++)
458 size_t l = strnlen(names[i],
sizeof(tmp));
459 if (i && o + 1 <=
sizeof(tmp))
463 if (o + l <=
sizeof(tmp))
465 memcpy(tmp + o, names[i], l);
474static int negotiate(
const uint8_t *slist, uint32_t slen,
const char *
const *prefs,
size_t nprefs)
476 for (
size_t i = 0; i < nprefs; i++)
478 if (namelist_has(slist, slen, prefs[i]))
488static bool mpint_to_fixed(
const uint8_t *v, uint32_t vlen, uint8_t *out,
size_t width)
491 while (i < vlen && v[i] == 0)
495 uint32_t mag = vlen - i;
500 memset(out, 0, width);
501 memcpy(out + (width - mag), v + i, mag);
505static bool build_kexinit(
void)
507 Wr w = {s_cli.i_c,
sizeof(s_cli.i_c), 0,
true};
511 w_bytes(&w, cookie, 16);
512 w_namelist(&w, KEX_NAMES,
sizeof(KEX_NAMES) /
sizeof(KEX_NAMES[0]));
513 w_namelist(&w, HOSTKEY_NAMES,
sizeof(HOSTKEY_NAMES) /
sizeof(HOSTKEY_NAMES[0]));
514 w_namelist(&w, CIPHER_NAMES,
sizeof(CIPHER_NAMES) /
sizeof(CIPHER_NAMES[0]));
515 w_namelist(&w, CIPHER_NAMES,
sizeof(CIPHER_NAMES) /
sizeof(CIPHER_NAMES[0]));
516 w_namelist(&w, MAC_NAMES,
sizeof(MAC_NAMES) /
sizeof(MAC_NAMES[0]));
517 w_namelist(&w, MAC_NAMES,
sizeof(MAC_NAMES) /
sizeof(MAC_NAMES[0]));
528 s_cli.i_c_len = (uint16_t)w.off;
529 return cli_send(s_cli.i_c, s_cli.i_c_len);
533static bool build_kex_public(
void)
537 case CliKex::CURVE25519:
542 case CliKex::ECDH_P256:
544 for (
int tries = 0; tries < 8; tries++)
554 case CliKex::DH_GROUP14: {
564 pc_secure_wipe(&x,
sizeof(x));
565 pc_secure_wipe(&e,
sizeof(e));
569 case CliKex::MLKEM768_X25519: {
572 uint8_t d[32], z[32], ek[MLKEM768_EK_BYTES];
575 pc_mlkem768_keygen(d, z, ek, s_cli.hyb.mlkem_dk);
576 pc_secure_wipe(d,
sizeof(d));
577 pc_secure_wipe(z,
sizeof(z));
578 pc_secure_wipe(ek,
sizeof(ek));
585#if PC_ENABLE_SSH_SNTRUP761
586 case CliKex::SNTRUP761_X25519: {
591 uint8_t *pk = (uint8_t *)
scratch_alloc(PC_SNTRUP761_PK_BYTES, 1);
596 pc_sntrup761_keypair(pk, s_cli.hyb.sntrup_sk);
609static bool handle_server_kexinit(
const uint8_t *p,
size_t len)
611 if (len >
sizeof(s_cli.i_s))
615 memcpy(s_cli.i_s, p, len);
616 s_cli.i_s_len = (uint16_t)len;
618 Rd r = {p, len, 0,
true};
621 uint32_t kn, hn, cn, mn;
622 const uint8_t *kex = r_string(&r, &kn);
623 const uint8_t *hk = r_string(&r, &hn);
624 const uint8_t *ec = r_string(&r, &cn);
626 const uint8_t *mc = r_string(&r, &mn);
632 int ki = negotiate(kex, kn, KEX_NAMES,
sizeof(KEX_NAMES) /
sizeof(KEX_NAMES[0]));
633 int hi = negotiate(hk, hn, HOSTKEY_NAMES,
sizeof(HOSTKEY_NAMES) /
sizeof(HOSTKEY_NAMES[0]));
634 int ci = negotiate(ec, cn, CIPHER_NAMES,
sizeof(CIPHER_NAMES) /
sizeof(CIPHER_NAMES[0]));
635 if (ki < 0 || hi < 0 || ci < 0)
640 s_cli.kex = KEX_OF[ki];
641 s_cli.hostkey = (CliHostkey)hi;
643 s_cli.cipher = cipher_of[ci];
644 PC_LOGI(LOG_TUNNEL_NEGOTIATED, KEX_NAMES[ki], HOSTKEY_NAMES[hi], CIPHER_NAMES[ci]);
649 int mi = negotiate(mc, mn, MAC_NAMES,
sizeof(MAC_NAMES) /
sizeof(MAC_NAMES[0]));
656 s_cli.mac = mac_of[mi];
659 if (!build_kex_public())
665 if (s_cli.kex == CliKex::MLKEM768_X25519)
669 const uint8_t *ek = s_cli.hyb.mlkem_dk + 1152;
670 const size_t clen = MLKEM768_EK_BYTES + 32;
671 const size_t plen = 1 + 4 + clen;
678 Wr w = {out, plen, 0,
true};
680 w_u32(&w, (uint32_t)clen);
681 w_bytes(&w, ek, MLKEM768_EK_BYTES);
682 w_bytes(&w, s_cli.qc, 32);
683 bool ok = w.ok && cli_send(out, w.off);
688#if PC_ENABLE_SSH_SNTRUP761
689 if (s_cli.kex == CliKex::SNTRUP761_X25519)
694 const uint8_t *pk = s_cli.hyb.sntrup_sk + PC_SNTRUP761_SK_PK_OFFSET;
695 const size_t clen = PC_SNTRUP761_PK_BYTES + 32;
696 const size_t plen = 1 + 4 + clen;
703 Wr w = {out, plen, 0,
true};
705 w_u32(&w, (uint32_t)clen);
706 w_bytes(&w, pk, PC_SNTRUP761_PK_BYTES);
707 w_bytes(&w, s_cli.qc, 32);
708 bool ok = w.ok && cli_send(out, w.off);
715 uint8_t out[1 + 4 + 260];
716 Wr w = {out,
sizeof(out), 0,
true};
718 if (s_cli.kex == CliKex::DH_GROUP14)
722 while (i < s_cli.qc_len && s_cli.qc[i] == 0)
726 size_t mag = s_cli.qc_len - i;
727 bool pad = (mag > 0 && (s_cli.qc[i] & 0x80) != 0);
728 w_u32(&w, (uint32_t)(mag + (pad ? 1 : 0)));
733 w_bytes(&w, s_cli.qc + i, mag);
737 w_string(&w, s_cli.qc, s_cli.qc_len);
739 return w.ok && cli_send(out, w.off);
747static bool compute_k(
const uint8_t *srv_pub, uint32_t srv_pub_len, uint8_t k_be[256])
749 memset(k_be, 0, 256);
752 case CliKex::CURVE25519: {
753 if (srv_pub_len != 32)
759 memcpy(k_be + (256 - 32), k32, 32);
760 pc_secure_wipe(k32, 32);
763 case CliKex::ECDH_P256: {
773 memcpy(k_be + (256 - 32), k32, 32);
774 pc_secure_wipe(k32, 32);
777 case CliKex::DH_GROUP14: {
787 pc_secure_wipe(&x,
sizeof(x));
788 pc_secure_wipe(&K,
sizeof(K));
792 case CliKex::MLKEM768_X25519: {
795 if (srv_pub_len != MLKEM768_CT_BYTES + 32)
799 uint8_t k_pq[32], k_cl[32];
800 pc_mlkem768_decaps(s_cli.hyb.mlkem_dk, srv_pub, k_pq);
801 pc_x25519(k_cl, s_cli.kex_priv, srv_pub + MLKEM768_CT_BYTES);
807 pc_secure_wipe(k_pq,
sizeof(k_pq));
808 pc_secure_wipe(k_cl,
sizeof(k_cl));
812#if PC_ENABLE_SSH_SNTRUP761
813 case CliKex::SNTRUP761_X25519: {
816 if (srv_pub_len != PC_SNTRUP761_CT_BYTES + 32)
820 uint8_t k_pq[PC_SNTRUP761_SS_BYTES], k_cl[32];
821 pc_sntrup761_dec(s_cli.hyb.sntrup_sk, srv_pub, k_pq);
822 pc_x25519(k_cl, s_cli.kex_priv, srv_pub + PC_SNTRUP761_CT_BYTES);
828 pc_secure_wipe(k_pq,
sizeof(k_pq));
829 pc_secure_wipe(k_cl,
sizeof(k_cl));
839static size_t compute_h(
const uint8_t *ks, uint32_t ks_len,
const uint8_t *srv_pub, uint32_t srv_pub_len,
842 const bool is512 = cli_kex_is_sha512(s_cli.kex);
844 ssh_kexhash_init(&c, is512);
845 hash_string(&c, (
const uint8_t *)CLIENT_BANNER, strnlen(CLIENT_BANNER,
sizeof(CLIENT_BANNER)));
846 hash_string(&c, (
const uint8_t *)s_cli.v_s, s_cli.v_s_len);
847 hash_string(&c, s_cli.i_c, s_cli.i_c_len);
848 hash_string(&c, s_cli.i_s, s_cli.i_s_len);
849 hash_string(&c, ks, ks_len);
850#if PC_ENABLE_PQC_KEX || PC_ENABLE_SSH_SNTRUP761
852 const uint8_t *cpk =
nullptr;
853 size_t cpk_len = 0, k_slen = 0;
855 if (s_cli.kex == CliKex::MLKEM768_X25519)
858 cpk = s_cli.hyb.mlkem_dk + 1152;
859 cpk_len = MLKEM768_EK_BYTES;
863#if PC_ENABLE_SSH_SNTRUP761
864 if (s_cli.kex == CliKex::SNTRUP761_X25519)
867 cpk = s_cli.hyb.sntrup_sk + PC_SNTRUP761_SK_PK_OFFSET;
868 cpk_len = PC_SNTRUP761_PK_BYTES;
876 uint32_t clen = (uint32_t)(cpk_len + 32);
877 uint8_t lb[4] = {(uint8_t)(clen >> 24), (uint8_t)(clen >> 16), (uint8_t)(clen >> 8), (uint8_t)clen};
878 ssh_kexhash_update(&c, lb, 4);
879 ssh_kexhash_update(&c, cpk, cpk_len);
880 ssh_kexhash_update(&c, s_cli.qc, 32);
881 hash_string(&c, srv_pub, srv_pub_len);
882 hash_string(&c, k_be + (256 - k_slen), k_slen);
886 if (s_cli.kex == CliKex::DH_GROUP14)
888 hash_mpint(&c, s_cli.qc, s_cli.qc_len);
889 hash_mpint(&c, srv_pub, srv_pub_len);
890 hash_mpint(&c, k_be, 256);
894 hash_string(&c, s_cli.qc, s_cli.qc_len);
895 hash_string(&c, srv_pub, srv_pub_len);
896 hash_mpint(&c, k_be, 256);
898 return ssh_kexhash_final(&c, H);
902static bool verify_host_sig(
const uint8_t *ks, uint32_t ks_len,
const uint8_t *sig, uint32_t sig_len,
const uint8_t *H,
905 Rd rk = {ks, ks_len, 0,
true};
907 const uint8_t *ktype = r_string(&rk, &tn);
908 Rd rs = {sig, sig_len, 0,
true};
910 const uint8_t *stype = r_string(&rs, &sn);
911 if (!rk.ok || !rs.ok)
916 switch (s_cli.hostkey)
918 case CliHostkey::ED25519: {
920 const uint8_t *pub = r_string(&rk, &pn);
922 const uint8_t *raw = r_string(&rs, &rl);
923 return rk.ok && rs.ok && pn == 32 && rl == 64 &&
pc_ed25519_verify(pub, H, h_len, raw);
925 case CliHostkey::ECDSA_P256: {
929 const uint8_t *q = r_string(&rk, &qn);
932 const uint8_t *blob = r_string(&rs, &bl);
937 Rd rb = {blob, bl, 0,
true};
939 const uint8_t *rr = r_string(&rb, &rlen);
940 const uint8_t *ss = r_string(&rb, &slen);
942 if (!rb.ok || !mpint_to_fixed(rr, rlen, raw, 32) || !mpint_to_fixed(ss, slen, raw + 32, 32))
948 case CliHostkey::RSA_SHA256:
949 case CliHostkey::RSA_SHA512: {
952 const uint8_t *e = r_string(&rk, &elen);
953 const uint8_t *n = r_string(&rk, &nlen);
955 const uint8_t *raw = r_string(&rs, &rawlen);
958 uint8_t e4[4], n256[256];
959 if (!rk.ok || !rs.ok || !mpint_to_fixed(e, elen, e4, 4) || !mpint_to_fixed(n, nlen, n256, 256))
964 return pc_rsa_verify(n256, e4, H, h_len, raw, rawlen, h) == 0;
970static bool handle_kexdh_reply(
const uint8_t *p,
size_t len)
972 Rd r = {p, len, 0,
true};
978 const uint8_t *ks = r_string(&r, &ks_len);
980 const uint8_t *srv_pub = r_string(&r, &sp_len);
982 const uint8_t *sig = r_string(&r, &sig_len);
994 if (memcmp(fp, s_cli.cfg.host_pin, 32) != 0)
996 cli_fail(
"relay host key does not match the pin");
1001 if (!compute_k(srv_pub, sp_len, k_be))
1007 const size_t h_len = compute_h(ks, ks_len, srv_pub, sp_len, k_be, H);
1009 if (!verify_host_sig(ks, ks_len, sig, sig_len, H, h_len))
1011 pc_secure_wipe(k_be,
sizeof(k_be));
1012 cli_fail(
"relay signature verification failed");
1016 if (!s_cli.have_sid)
1018 memcpy(s_cli.session_id, H, h_len);
1019 s_cli.session_id_len = (uint8_t)h_len;
1020 s_cli.have_sid =
true;
1027 const bool is512 = cli_kex_is_sha512(s_cli.kex);
1028 bool k_is_string =
false;
1029#if PC_ENABLE_PQC_KEX
1030 k_is_string = k_is_string || (s_cli.kex == CliKex::MLKEM768_X25519);
1032#if PC_ENABLE_SSH_SNTRUP761
1033 k_is_string = k_is_string || (s_cli.kex == CliKex::SNTRUP761_X25519);
1035 ssh_dh_derive_keys_sid(SSH_CLI_SLOT, k_be, H, s_cli.session_id, s_cli.cipher, s_cli.mac, k_is_string, h_len,
1036 s_cli.session_id_len, is512);
1037 pc_secure_wipe(k_be,
sizeof(k_be));
1038 pc_secure_wipe(s_cli.kex_priv,
sizeof(s_cli.kex_priv));
1039#if PC_ENABLE_PQC_KEX || PC_ENABLE_SSH_SNTRUP761
1040 pc_secure_wipe((uint8_t *)&s_cli.hyb,
sizeof(s_cli.hyb));
1044 if (!cli_send(&nk, 1))
1056static bool send_service_request(
void)
1058 uint8_t out[1 + 4 + 12];
1059 Wr w = {out,
sizeof(out), 0,
true};
1061 w_cstr(&w,
"ssh-userauth");
1062 return w.ok && cli_send(out, w.off);
1065static bool send_userauth_publickey(
void)
1067 const char *user = s_cli.cfg.user;
1072 uint8_t pkblob[4 + 11 + 4 + 32];
1073 Wr pw = {pkblob,
sizeof(pkblob), 0,
true};
1074 w_cstr(&pw, NAME_ED25519);
1075 w_string(&pw, pub, 32);
1085 Wr sd = {signed_data,
sizeof(signed_data), 0,
true};
1086 w_string(&sd, s_cli.session_id, s_cli.session_id_len);
1089 w_cstr(&sd,
"ssh-connection");
1090 w_cstr(&sd,
"publickey");
1092 w_cstr(&sd, NAME_ED25519);
1093 w_string(&sd, pkblob, pw.off);
1103 uint8_t sigblob[4 + 11 + 4 + 64];
1104 Wr sg = {sigblob,
sizeof(sigblob), 0,
true};
1105 w_cstr(&sg, NAME_ED25519);
1106 w_string(&sg, sig, 64);
1114 Wr w = {out,
sizeof(out), 0,
true};
1117 w_cstr(&w,
"ssh-connection");
1118 w_cstr(&w,
"publickey");
1120 w_cstr(&w, NAME_ED25519);
1121 w_string(&w, pkblob, pw.off);
1122 w_string(&w, sigblob, sg.off);
1123 return w.ok && cli_send(out, w.off);
1130static bool send_tcpip_forward(
void)
1133 Wr w = {out,
sizeof(out), 0,
true};
1135 w_cstr(&w,
"tcpip-forward");
1137 w_cstr(&w, s_cli.cfg.bind_addr ? s_cli.cfg.bind_addr :
"");
1138 w_u32(&w, s_cli.cfg.bind_port);
1139 return w.ok && cli_send(out, w.off);
1146#define SSH_CLI_WINDOW 32768u
1147#define SSH_CLI_MAXPKT 16384u
1149static void handle_channel_open(
const uint8_t *p,
size_t len)
1151 Rd r = {p, len, 0,
true};
1154 const uint8_t *type = r_string(&r, &tn);
1155 uint32_t their_id = r_u32(&r);
1156 uint32_t their_win = r_u32(&r);
1158 if (!r.ok || tn != 15 || memcmp(type,
"forwarded-tcpip", 15) != 0)
1162 Wr w = {out,
sizeof(out), 0,
true};
1164 w_u32(&w, their_id);
1166 w_cstr(&w,
"only forwarded-tcpip");
1170 cli_send(out, w.off);
1176 CliChannel *ch = chan_alloc();
1180 Wr w = {out,
sizeof(out), 0,
true};
1182 w_u32(&w, their_id);
1188 cli_send(out, w.off);
1194 int lc =
pc_client_open(
"127.0.0.1", s_cli.cfg.local_port, 3000);
1195 PC_LOGD(LOG_TUNNEL_FWD_OPEN, (uint32_t)s_cli.cfg.local_port, (int64_t)lc);
1199 Wr w = {out,
sizeof(out), 0,
true};
1201 w_u32(&w, their_id);
1203 w_cstr(&w,
"local connect failed");
1207 cli_send(out, w.off);
1213 ch->remote_id = their_id;
1214 ch->local_id = s_cli.next_chan_id++;
1215 ch->send_win = their_win;
1216 ch->recv_win = SSH_CLI_WINDOW;
1218 ch->eof_sent =
false;
1219 ch->relay_eof =
false;
1222 Wr w = {out,
sizeof(out), 0,
true};
1224 w_u32(&w, their_id);
1225 w_u32(&w, ch->local_id);
1226 w_u32(&w, SSH_CLI_WINDOW);
1227 w_u32(&w, SSH_CLI_MAXPKT);
1230 cli_send(out, w.off);
1234static void channel_close(CliChannel *ch)
1236 if (!ch || !ch->used)
1241 Wr w = {out,
sizeof(out), 0,
true};
1243 w_u32(&w, ch->remote_id);
1246 cli_send(out, w.off);
1248 if (ch->local_cid >= 0)
1252 memset(ch, 0,
sizeof(*ch));
1257static void handle_channel_data(
const uint8_t *p,
size_t len)
1259 Rd r = {p, len, 0,
true};
1261 uint32_t rid = r_u32(&r);
1263 const uint8_t *d = r_string(&r, &dn);
1264 CliChannel *ch = chan_by_local(rid);
1269 if (ch->local_cid >= 0 && dn)
1275 if (ch->recv_win >= dn)
1283 if (ch->recv_win < SSH_CLI_WINDOW / 2)
1285 uint32_t add = SSH_CLI_WINDOW - ch->recv_win;
1287 Wr w = {out,
sizeof(out), 0,
true};
1289 w_u32(&w, ch->remote_id);
1291 if (w.ok && cli_send(out, w.off))
1293 ch->recv_win += add;
1300static void pump_channel(CliChannel *ch)
1302 if (!ch->used || ch->local_cid < 0)
1307 while (ch->send_win > 0)
1309 size_t want =
sizeof(buf);
1310 if (want > ch->send_win)
1312 want = ch->send_win;
1314 if (want > SSH_CLI_MAXPKT)
1316 want = SSH_CLI_MAXPKT;
1324 Wr w = {hdr,
sizeof(hdr), 0,
true};
1326 w_u32(&w, ch->remote_id);
1327 w_u32(&w, (uint32_t)got);
1329 uint8_t payload[9 +
sizeof(buf)];
1330 memcpy(payload, hdr, w.off);
1331 memcpy(payload + w.off, buf, got);
1332 if (!cli_send(payload, w.off + got))
1336 ch->send_win -= (uint32_t)got;
1343 if ((local_done || ch->relay_eof) && !ch->eof_sent)
1346 Wr w = {out,
sizeof(out), 0,
true};
1348 w_u32(&w, ch->remote_id);
1351 cli_send(out, w.off);
1353 ch->eof_sent =
true;
1359static void pump_local_to_relay(
void)
1363 if (s_cli.chan[i].used)
1365 pump_channel(&s_cli.chan[i]);
1374static void cli_msg_handler(uint8_t slot, uint8_t type,
const uint8_t *payload,
size_t len)
1377 switch (s_cli.phase)
1379 case CliPhase::KEXINIT:
1382 if (handle_server_kexinit(payload, len))
1384 s_cli.phase = CliPhase::KEXREPLY;
1388 cli_fail(
"KEXINIT negotiation failed");
1392 case CliPhase::KEXREPLY:
1395 if (handle_kexdh_reply(payload, len))
1397 s_cli.phase = CliPhase::NEWKEYS;
1399 else if (s_cli.phase != CliPhase::FAILED)
1401 cli_fail(
"KEXDH_REPLY invalid");
1405 case CliPhase::NEWKEYS:
1410 if (send_service_request())
1412 s_cli.phase = CliPhase::SERVICE;
1416 cli_fail(
"service request send failed");
1420 case CliPhase::SERVICE:
1423 if (send_userauth_publickey())
1425 s_cli.phase = CliPhase::AUTH;
1429 cli_fail(
"userauth send failed");
1433 case CliPhase::AUTH:
1436 if (send_tcpip_forward())
1438 s_cli.phase = CliPhase::FORWARD;
1442 cli_fail(
"tcpip-forward send failed");
1447 cli_fail(
"authentication rejected by the relay");
1450 case CliPhase::FORWARD:
1453 s_cli.phase = CliPhase::OPEN;
1454 s_cli.state = pc_ssh_tunnel_state::PC_TUN_UP;
1455 PC_LOGI(LOG_TUNNEL_UP, (uint32_t)s_cli.cfg.bind_port);
1459 cli_fail(
"relay refused the remote forward");
1462 case CliPhase::OPEN:
1466 handle_channel_open(payload, len);
1469 handle_channel_data(payload, len);
1472 Rd r = {payload, len, 0,
true};
1474 uint32_t rid = r_u32(&r);
1475 uint32_t add = r_u32(&r);
1476 CliChannel *ch = chan_by_local(rid);
1479 ch->send_win += add;
1488 Rd r = {payload, len, 0,
true};
1490 uint32_t rid = r_u32(&r);
1491 CliChannel *ch = chan_by_local(rid);
1494 ch->relay_eof =
true;
1499 Rd r = {payload, len, 0,
true};
1501 uint32_t rid = r_u32(&r);
1504 channel_close(chan_by_local(rid));
1510 Rd r = {payload, len, 0,
true};
1514 uint8_t wr = r_u8(&r);
1535bool pc_ssh_tunnel_begin(
const pc_ssh_tunnel_cfg *cfg)
1537 if (!cfg || !cfg->host || !cfg->user || !cfg->auth_seed || !cfg->host_pin)
1542 pc_ssh_tunnel_end();
1543 memset(&s_cli, 0,
sizeof(s_cli));
1547 s_cli.chan[i].local_cid = -1;
1549 s_cli.next_chan_id = 1;
1556 uint16_t port = cfg->port ? cfg->port : 22;
1560 s_cli.state = pc_ssh_tunnel_state::PC_TUN_FAILED;
1566 ssh_keymat_wipe(SSH_CLI_SLOT);
1570 pc_sb sb_banner = {banner,
sizeof(banner), 0,
true};
1574 if (n <= 0 || !
pc_client_send(s_cli.cid, (
const uint8_t *)banner, (
size_t)n))
1576 cli_fail(
"banner send failed");
1579 s_cli.phase = CliPhase::BANNER;
1580 s_cli.state = pc_ssh_tunnel_state::PC_TUN_CONNECTING;
1581 s_cli.deadline_ms =
pc_millis() + 15000;
1586static void drain_banner(
const uint8_t *data,
size_t len,
size_t *consumed)
1589 for (
size_t i = 0; i < len; i++)
1591 uint8_t ch = data[i];
1592 if (s_cli.banner_len <
sizeof(s_cli.banner))
1594 s_cli.banner[s_cli.banner_len++] = ch;
1599 uint16_t l = s_cli.banner_len;
1600 while (l && (s_cli.banner[l - 1] ==
'\n' || s_cli.banner[l - 1] ==
'\r'))
1604 if (l >= 4 && memcmp(s_cli.banner,
"SSH-", 4) == 0)
1606 memcpy(s_cli.v_s, s_cli.banner, l);
1609 s_cli.phase = CliPhase::KEXINIT;
1610 if (!build_kexinit())
1612 cli_fail(
"KEXINIT send failed");
1616 s_cli.banner_len = 0;
1622void pc_ssh_tunnel_poll(
void)
1624 if (s_cli.cid < 0 || s_cli.phase == CliPhase::IDLE || s_cli.phase == CliPhase::FAILED)
1631 cli_fail(
"relay closed the connection");
1634 if (s_cli.state == pc_ssh_tunnel_state::PC_TUN_CONNECTING && (int32_t)(
pc_millis() - s_cli.deadline_ms) > 0)
1636 cli_fail(
"handshake timed out");
1645 if (s_cli.phase == CliPhase::BANNER)
1647 size_t consumed = 0;
1648 drain_banner(buf, got, &consumed);
1651 if (off < got && s_cli.phase != CliPhase::FAILED)
1653 if (
ssh_pkt_recv(SSH_CLI_SLOT, buf + off, got - off, cli_msg_handler) != 0)
1655 cli_fail(
"packet error (MAC / framing)");
1660 if (s_cli.phase == CliPhase::OPEN)
1662 pump_local_to_relay();
1666void pc_ssh_tunnel_end(
void)
1670 if (s_cli.chan[i].used && s_cli.chan[i].local_cid >= 0)
1679 ssh_keymat_wipe(SSH_CLI_SLOT);
1680 pc_secure_wipe(s_cli.kex_priv,
sizeof(s_cli.kex_priv));
1681 memset(&s_cli, 0,
sizeof(s_cli));
1683 s_cli.phase = CliPhase::IDLE;
1684 s_cli.state = pc_ssh_tunnel_state::PC_TUN_IDLE;
1687pc_ssh_tunnel_state pc_ssh_tunnel_state_get(
void)
1692bool pc_ssh_tunnel_up(
void)
1694 return s_cli.state == pc_ssh_tunnel_state::PC_TUN_UP;
1699bool pc_ssh_tunnel_begin(
const pc_ssh_tunnel_cfg *)
1703void pc_ssh_tunnel_poll(
void)
1706void pc_ssh_tunnel_end(
void)
1709pc_ssh_tunnel_state pc_ssh_tunnel_state_get(
void)
1711 return pc_ssh_tunnel_state::PC_TUN_IDLE;
1713bool pc_ssh_tunnel_up(
void)
1721void pc_ssh_tunnel_pubkey(
const uint8_t seed[32], uint8_t pub[32])
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.
#define PC_SSH_CLIENT_MAX_CHANNELS
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
int pc_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
size_t pc_client_available(int)
Wire bytes currently buffered and ready to read.
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
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_verify(const uint8_t pub[PC_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[PC_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
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_PUB_LEN
P-256 uncompressed public point length: 0x04 || X || Y.
void pc_ed25519_pubkey(uint8_t pub[32], const uint8_t seed[32])
bool pc_ed25519_verify(const uint8_t pub[32], const uint8_t *msg, size_t mlen, const uint8_t sig[64])
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.
@ PC_FK_LIT
literal text from lit; takes no argument
Abstract logging whose disabled levels cost nothing at all (PC_LOG_LEVEL).
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).
int pc_rsa_verify(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len, const uint8_t *sig, size_t sig_len, pc_rsa_hash hash)
Verify an RSA-2048 PKCS#1 v1.5 signature over msg.
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.
size_t scratch_mark(void)
Capture the current arena offset (a savepoint for scratch_release()).
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
void scratch_release(size_t mark)
Reclaim everything allocated since mark (LIFO).
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
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.
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.
Outbound SSH client + reverse tunnel (PC_ENABLE_SSH_CLIENT).
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).
DH-group14-SHA256 key exchange (RFC 4253 §8 + RFC 8268).
One key-exchange digest that dispatches SHA-256 or SHA-512 by the negotiated method.
#define SSH_KEXHASH_MAX_LEN
longest exchange-hash / session_id (SHA-512)
SSH session key material - types, pools, and security model.
@ 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.
int ssh_pkt_send(uint8_t i, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len, size_t out_cap)
Build and send one SSH binary packet.
void ssh_pkt_init(uint8_t i)
Initialize the packet state for SSH connection slot i.
void ssh_pkt_set_client(uint8_t i)
Mark slot i as the SSH client role (call once, right after ssh_pkt_init).
int ssh_pkt_recv(uint8_t i, const uint8_t *data, size_t len, ssh_msg_handler_t handler)
Receive and process one or more SSH binary packets from data.
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_MSG_CHANNEL_EOF
#define SSH_MSG_CHANNEL_DATA
#define SSH_MSG_CHANNEL_OPEN_CONFIRM
#define SSH_MSG_USERAUTH_REQUEST
#define SSH_MSG_SERVICE_ACCEPT
#define SSH_MSG_KEXDH_REPLY
#define SSH_MSG_CHANNEL_OPEN
#define SSH_MSG_CHANNEL_OPEN_FAILURE
#define SSH_MSG_REQUEST_SUCCESS
#define SSH_MSG_SERVICE_REQUEST
#define SSH_MSG_KEXDH_INIT
#define SSH_MSG_USERAUTH_SUCCESS
#define SSH_MSG_CHANNEL_CLOSE
#define SSH_MSG_USERAUTH_FAILURE
#define SSH_MSG_REQUEST_FAILURE
#define SSH_MSG_GLOBAL_REQUEST
#define SSH_MSG_CHANNEL_WINDOW_ADJUST
SSH RSA host-key layer: NVS-backed host key, host-key signing, and "ssh-rsa" blob encoding.
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_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).
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).
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
One field of a frame. Frames are static const pc_field[], so they live in rodata.
Bump-append target; ok latches false once an append would overflow cap.
Streaming SHA-256 context.
Streaming SHA-512 context.
void pc_worker_set_self(int id)
Bind the calling task/thread to worker id id (worker entry / tests).
Layer 5 (Session) - server worker identity.