|
DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
|
DeterministicESPAsyncWebServer includes a complete SSH-2.0 server protocol: identification-string exchange, algorithm negotiation, Diffie-Hellman key exchange, user authentication (password and publickey), and the connection/channel protocol - all built on constant-memory, side-channel-aware primitives.
The message state machine is driven by a single transport-agnostic dispatcher (`ssh_server.cpp`) that consumes decrypted message payloads and emits responses through a callback, so it is fully unit-testable off-target. The TCP glue (`ssh_conn.cpp`) binds a `PROTO_SSH` connection to a session slot, pumps ring-buffer bytes through the banner exchange and binary-packet layer, and writes responses back to the socket.
Handshake: banner exchange →
KEXINITnegotiation →KEXDH→NEWKEYS→ssh-userauth(publickey or password) →ssh-connectionsession channel, with transparent in-session re-keys.
ssh_kex_set_prefer_rsa(), default RSA/DH) and picks the first mutually supported one it holds a key for. A stock ssh client gets modern crypto; a device that wants the ESP32-accelerated path forces RSA/DHd_S * Q_C, hashed as an mpint with Q_C/Q_S as 65-byte point strings. ESP32 uses mbedTLS (hardware-accelerated, side-channel-hardened mbedtls_ecdh_compute_shared); the native test path is the same software P-256 as the host key, pinned to the RFC 5903 §8.1 shared-secret vectorschacha20-poly1305@openssh.com** encryption - OpenSSH's default AEAD cipher (the negotiated preference); ChaCha20 (RFC 8439) + Poly1305 with the length field encrypted under a separate key, verified against the RFC 8439 vectors and cross-checked byte-for-byte against OpenSSL. A stock ssh client negotiates it; HW-verified against OpenSSH on an ESP32-S3aes256-gcm@openssh.com** encryption (RFC 5647) - AES-256-GCM AEAD, the second negotiated preference; the 4-byte packet length is sent in the clear as additional authenticated data, the packet body is sealed, and the 96-bit nonce's invocation counter advances per packet. AES block hardware-accelerated on ESP32 via mbedTLS (GHASH in software), software AES-256 for native tests; seal/open verified byte-for-byte against the NIST/McGrew AES-256-GCM Test Case 16 vectorhmac-sha2-256-etm@openssh.com** / **hmac-sha2-512-etm@openssh.com** (OpenSSH's preferred MACs for the aes cipher; the length is authenticated in the clear and the MAC is verified before any decryption) - MAC-verify-before-use invariant; the chacha20-poly1305 AEAD carries its own implicit MAC. All HW-verified against OpenSSH on an ESP32-S3ssh_hostkey_ed25519_set() alongside or instead of the RSA host keyssh-rsa key and negotiated as separate server_host_key_algorithms (rsa-sha2-512 preferred). Real on both platforms (ESP32 mbedTLS, native full-width modexp), HW-accelerated on ESP32ssh_hostkey_ecdsa_set(). ESP32 uses mbedTLS (hardware-accelerated, side-channel-hardened); the native test path is a self-contained software P-256 with RFC 6979 deterministic signing, pinned byte-for-byte to the RFC 6979 A.2.5 (P-256/SHA-256) vectorsrsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp256 and ssh-ed25519 signatures are verified for real on both platforms (the RSA hash is chosen from the client's signature-algorithm name); an application callback authorizes keys per userDETWS_SSH_ALLOW_PASSWORD=0 for publickey-only hardeningshell/exec/pty-req accepted; inbound channel data surfaced to the app as a raw byte stream, with RFC 4254 §5.2 window flow controldirect-tcpip, the ssh -L local forward) - opt-in via DETWS_SSH_PORT_FORWARD; the ssh_forward owner opens the outbound connection through the client transport and bridges bytes both ways, with an optional target-allow policy callbackssh client (no algorithm overrides): the RX ring and KEXINIT store hold a full modern client KEXINIT, and **ext-info / server-sig-algs** (RFC 8308) is advertised (rsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp256 and ssh-ed25519, in preference order) so the client picks a key type it can offer. Depending on the server preference the client negotiates curve25519-sha256 + ssh-ed25519 (default modern client choice) or diffie-hellman-group14-sha256 + rsa-sha2-512. HW-validated on an ESP32-S3 with a stock OpenSSH client on both suites (curve25519/ed25519 and DH/RSA), including an ed25519-only client key| Component | Standard | Status |
|---|---|---|
| SHA-256 | FIPS 180-4 | Implemented; HW-accelerated on ESP32 (mbedtls streaming + one-shot), software on native |
| HMAC-SHA2-256 | RFC 2104, RFC 6668 | Implemented; verify-before-use, constant-time MAC compare |
| AES-256-CTR | FIPS 197, RFC 4344 | Implemented (HW via mbedTLS on ESP32; software AES on native) |
AES-256-GCM (@openssh.com) | FIPS 197, RFC 5647, SP 800-38D | Implemented AEAD (length-in-clear as AAD, per-packet invocation counter); AES HW via mbedTLS on ESP32, GHASH in software; KAT vs NIST/McGrew Test Case 16 |
| DH group14 (modexp) | RFC 3526, RFC 8268 | Implemented (HW mbedtls_mpi on ESP32; software Montgomery on native) |
| curve25519-sha256 KEX (X25519) | RFC 8731, RFC 7748 | Implemented - software radix-2^16 field math; field inversion on the ESP32 MPI accelerator (mbedtls_mpi_exp_mod), software Fermat on native |
| ecdh-sha2-nistp256 KEX | RFC 5656 §4, RFC 5903 | Implemented - P-256 ECDH shared secret (mpint), on-curve peer-point check; ESP32 mbedTLS (HW), native software P-256. Byte-exact vs RFC 5903 §8.1 vectors |
| X25519 low-order point reject | RFC 7748 §6.1 | Implemented (rejects an all-zero shared secret) |
| DH public-value validation | RFC 4253 §8 | Implemented (rejects e/f outside 1 < v < p-1) |
| ssh-ed25519 host key + sign | RFC 8709, RFC 8032 | Implemented - deterministic Ed25519 over the exchange hash; SHA-512 HW on ESP32 / software on native |
| Ed25519 client-auth verify | RFC 8709, RFC 8032 §5.1.7 | Implemented - ssh-ed25519 client signatures verified for real on both platforms (KAT + tamper-reject tested) |
| ecdsa-sha2-nistp256 host key | RFC 5656, FIPS 186-4 | Implemented - ECDSA P-256 (SHA-256) over the exchange hash; ESP32 mbedTLS (HW), native software P-256. Sign byte-exact vs RFC 6979 A.2.5 vectors |
| ECDSA P-256 client-auth verify | RFC 5656 §3.1.2, RFC 6979 | Implemented - ecdsa-sha2-nistp256 client signatures (mpint r,s) verified on both platforms (KAT + e2e + genuine client-auth tested) |
| Session key derivation | RFC 4253 §7.2 | Implemented (six A–F keys from K, H, session_id) |
| Binary packet protocol | RFC 4253 §6 | Implemented - framing, ≥4-byte padding to 16-byte multiple, encrypt-then-MAC over seq‖plaintext, CTR length-peek with snapshot/restore |
| RSA public-key blob | RFC 4253 §6.6, RFC 8332 §3 | Implemented - blob type string is ssh-rsa for both rsa-sha2-256 and rsa-sha2-512 |
| RSA-SHA2-512 / -256 signing | RFC 8332, RFC 8017 §8.2 | Real on both platforms - ESP32 mbedtls_pk_sign (SHA-512/256 DigestInfo); native full-width m^d mod n. SHA-512 KAT byte-exact vs openssl; hash bound into the signature |
| RSA-SHA2-512 / -256 verification | RFC 8332, RFC 8017 §8.2 | Real on both platforms (small public exponent); hash chosen from the signature-algorithm name; validated against openssl-generated KATs |
| Version / KEXINIT negotiation | RFC 4253 §4.2, §7.1 | Implemented - banner exchange + crypto-agnostic negotiation (KEX method, host-key type, cipher [chacha/aes-gcm/aes-ctr], MAC, and s2c compression all negotiated to a runtime preference) |
| ext-info / server-sig-algs | RFC 8308 | Implemented - advertises accepted client-sig algorithms (rsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp256, ssh-ed25519) after NEWKEYS |
| Exchange hash + NEWKEYS | RFC 4253 §7.2, §8; RFC 8731 | Implemented - H over V_C,V_S,I_C,I_S,K_S, then e,f (DH mpints) or Q_C,Q_S (curve strings) then K; keys activate on NEWKEYS |
| User authentication | RFC 4252 §5, §7, §8 | Implemented - publickey (rsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp256, ssh-ed25519) and password methods |
| Connection / channel | RFC 4254 §5, §6 | Implemented - single session channel, data stream, window flow control |
| Re-keying | RFC 4253 §9 | Implemented - packet-count threshold; session id preserved across re-keys |
Two methods are offered. The application installs callbacks:
For publickey, the server first answers the no-signature probe with USERAUTH_PK_OK, then verifies the client's RSA-SHA2-256 signature over the session id and request before granting access.
Hardening: define DETWS_SSH_ALLOW_PASSWORD=0 to compile out password auth entirely. The password method is then refused and is not advertised in the USERAUTH_FAILURE method list; only publickey remains.
The server reads its RSA-2048 private key from NVS (namespace ssh_host_key, key priv_der) as a DER-encoded PKCS#1/PKCS#8 blob. Generate and store it once per device:
Generate a 2048-bit key and export DER (PKCS#8) on your workstation. The bundled generator does both and prints the matching .pub:
or by hand with the same tools:
(ssh_host.der must be ≤ `SSH_RSA_KEY_DER_MAX` bytes - ~1.2 KB for RSA-2048.) For a worked end-to-end walkthrough of both provisioning paths, see the 03.SSHHostKey example.
Write the DER blob into NVS from a one-time provisioning sketch:
Embed ssh_host.der as a byte array (e.g. xxd -i ssh_host.der), flash the provisioning sketch once, then flash your real firmware.
The matching public key for clients' known_hosts is derived from the same DER (ssh-keygen -y -f ssh_host.pem).
For hardware / interop testing there is a committed, public, insecure key at test/fixtures/ssh_test_host_key/: the DER as a ready-to-include byte array (ssh_test_host_key.h -> DETWS_SSH_TEST_HOST_KEY_DER) plus its .pub. Provision it in one step from a sketch:
Its private half is in the repo, so it authenticates nothing. Never use it in a product - deployments must generate their own key with the steps above and keep it secret.
session channel per connection (no port-forwarding / X11).#ifndef ARDUINO) and exist only for host tests. On ESP32 the hardware/mbedTLS paths are used. Native RSA signing and verification are both mathematically complete (validated by a sign→verify round-trip and an openssl KAT).All numbers use default configuration values.
| Symbol | Type | Notes |
|---|---|---|
ssh_pkt[MAX_SSH_CONNS] | BSS | Packet state + RX reassembly buffers |
ssh_keys[MAX_SSH_CONNS] | BSS | 2 AES-CTR contexts + 2 × 32-byte MAC keys (IV/counter lives inside each AES-CTR ctx) |
ssh_dh[MAX_SSH_CONNS] | BSS | DH scalars y, f, K (wiped post-KEX) + H |
ssh_sess[MAX_SSH_CONNS] | BSS | Handshake phase, V_C/I_C/I_S, session id |
crypto_work[1536] | BSS | Montgomery bignum scratch; zeroed after each use |
group14_p, group14_g | BSS | RFC 3526 prime + generator (constants) |
ssh_host_pubkey | BSS | RSA-2048 public key (n + e); no secret material |
Key material is excluded by design. AES-256 keys, HMAC keys, and the DH shared secret K live in ssh_keys[] / ssh_dh[]. The RSA private key is never in static memory - it exists only on the stack during `ssh_rsa_sign()` and is volatile-wiped before the function returns.
The BSS symbols (ssh_pkt, ssh_keys, ssh_dh) are separate linker symbols, so a linear buffer overflow from ssh_pkt[i].rx_buf cannot reach ssh_keys in a single stride.
See SECURITY.md for the full security treatment.