ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_client.h
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_client.h
6 * @brief Outbound SSH client + reverse tunnel (PC_ENABLE_SSH_CLIENT).
7 *
8 * The SSH server terminates inbound connections; this is the mirror - the device is the SSH
9 * *client*, dialing OUT to a relay and asking it to forward a port back. That is how a device behind
10 * NAT / a firewall stays reachable: it holds a `tcpip-forward` (RFC 4254 §7.1, the `ssh -R` seam) to
11 * a relay with a public address, and a connection to the relay's forwarded port is tunnelled back to
12 * a service on the device - the standards-track "secure machine bridge over untrusted networks".
13 *
14 * ── What it negotiates ─────────────────────────────────────────────────────────────────────
15 * kex : mlkem768x25519-sha256 (PQ/T hybrid, PC_ENABLE_PQC_KEX), curve25519-sha256,
16 * ecdh-sha2-nistp256, diffie-hellman-group14-sha256
17 * hostkey : ssh-ed25519, ecdsa-sha2-nistp256, rsa-sha2-512, rsa-sha2-256 - verified against a pin
18 * cipher : chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes256-ctr (+ hmac-sha2-256/512,
19 * ETM and E&M)
20 * auth : publickey, ssh-ed25519 (RFC 4252 §7) - the device signs with its own key
21 *
22 * The client offers the full suite and negotiates whatever the relay supports (RFC 4253 §7.1 order),
23 * so it interoperates with any modern SSH server without being kneecapped to one algorithm. It reuses
24 * the transport primitives the server already ships (the curve/DH/ECDH KEX cores, ML-KEM-768, ed25519
25 * / ECDSA / RSA verify, chacha-poly / AES-GCM / AES-CTR, the RFC 4253 §7.2 KDF, and the role-aware
26 * binary packet layer), so this file is the client-role state machine only, not a second crypto stack.
27 *
28 * ── Security ───────────────────────────────────────────────────────────────────────────────
29 * The relay's host key is **pinned** by fingerprint: the caller supplies the SHA-256 of the expected
30 * host-key blob (K_S), and the handshake aborts if the relay presents anything else (no
31 * trust-on-first-use, no accepting an unknown key). Pinning the fingerprint rather than a raw key is
32 * host-key-type agnostic - it works whether the relay's key is ed25519, ECDSA or RSA. The device
33 * authenticates with an ssh-ed25519 key of its own, whose public half the relay carries in
34 * `authorized_keys`.
35 *
36 * @author Douglas Quigg (dstroy0)
37 * @date 2026
38 */
39
40#ifndef PROTOCORE_SSH_CLIENT_H
41#define PROTOCORE_SSH_CLIENT_H
42
43#include "protocore_config.h"
44#include <stddef.h>
45#include <stdint.h>
46
47#if PC_ENABLE_SSH_CLIENT
48
49/** @brief How to reach the relay, who to log in as, and what to tunnel back. */
50struct pc_ssh_tunnel_cfg
51{
52 const char *host; ///< relay hostname or dotted-quad.
53 uint16_t port; ///< relay SSH port (0 => 22).
54 const char *user; ///< SSH username on the relay.
55 const uint8_t *auth_seed; ///< 32-byte ssh-ed25519 private seed the device authenticates with.
56 const uint8_t *host_pin; ///< 32-byte SHA-256 of the relay's host-key blob (K_S); handshake aborts on mismatch.
57 const char *bind_addr; ///< address the relay binds the forward on ("" / null => "" = all, "localhost", ...).
58 uint16_t bind_port; ///< remote port the relay listens on (tcpip-forward); connections there tunnel back.
59 uint16_t local_port; ///< local TCP port a tunnelled connection is bridged to (e.g. 80).
60};
61
62/** @brief Lifecycle phase of the tunnel, for observability. */
63enum class pc_ssh_tunnel_state : uint8_t
64{
65 PC_TUN_IDLE = 0, ///< not started.
66 PC_TUN_CONNECTING, ///< TCP + SSH handshake + auth in progress.
67 PC_TUN_UP, ///< authenticated and the remote forward is established.
68 PC_TUN_FAILED ///< the last attempt failed (host-key mismatch, auth, or transport).
69};
70
71/**
72 * @brief Start (or restart) the tunnel: connect to the relay, handshake, authenticate, and request
73 * the remote forward. Non-blocking after the initial connect; drive it with poll().
74 * @return true if the connection and handshake started; false on bad args or immediate failure.
75 *
76 * @warning Call begin() and poll() from the SAME task, and give that task enough stack for the
77 * negotiated KEX. The handshake's field arithmetic runs in the caller's task: curve25519/ed25519
78 * peak ~10.5 KB, and the mlkem768x25519 hybrid (PC_ENABLE_PQC_KEX) adds ML-KEM-768 for ~16 KB total.
79 * The Arduino loop() task's default 8 KB is NOT enough - run the tunnel from a dedicated task created
80 * with a >= 20480-byte stack (see the example). begin() claims a private scratch arena for the calling
81 * task, so poll() must run in that same task or the packet-decrypt tripwire fires.
82 */
83bool pc_ssh_tunnel_begin(const pc_ssh_tunnel_cfg *cfg);
84
85/**
86 * @brief Pump the tunnel: advance the handshake, service the relay's keepalives, accept
87 * forwarded-tcpip channels and bridge their bytes to/from the local service. Call every loop,
88 * from the same (adequately-stacked) task that called begin() - see the begin() @warning.
89 */
90void pc_ssh_tunnel_poll(void);
91
92/** @brief Tear the tunnel down and close the relay connection. */
93void pc_ssh_tunnel_end(void);
94
95/** @brief Current lifecycle state. */
96pc_ssh_tunnel_state pc_ssh_tunnel_state_get(void);
97
98/** @brief True once authenticated and the remote forward is live. */
99bool pc_ssh_tunnel_up(void);
100
101/**
102 * @brief Derive the ssh-ed25519 public key (32 bytes) from a private @p seed.
103 *
104 * Convenience for provisioning: print/serve this so it can be added to the relay's `authorized_keys`
105 * (as `ssh-ed25519 <base64(0x0000000b "ssh-ed25519" 0x00000020 <pub>)>`).
106 */
107void pc_ssh_tunnel_pubkey(const uint8_t seed[32], uint8_t pub[32]);
108
109#endif // PC_ENABLE_SSH_CLIENT
110#endif // PROTOCORE_SSH_CLIENT_H
User-facing configuration for ProtoCore.