DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_keymat.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_keymat.h
6 * @brief SSH session key material - types, pools, and security model.
7 *
8 * ═══════════════════════════════════════════════════════════════════════════
9 * SECURITY MODEL - READ BEFORE MODIFYING ANYTHING IN THIS FILE
10 * ═══════════════════════════════════════════════════════════════════════════
11 *
12 * The fundamental threat this layout defends against is **buffer-overflow
13 * key extraction**: an attacker who can cause an out-of-bounds read or write
14 * in the packet receive path (ssh_pool[].pkt_buf) should not be able to
15 * reach AES session keys or HMAC keys in the same memory operation.
16 *
17 * DEFENSE 1 - Physical BSS separation
18 * ─────────────────────────────────────
19 * All three pools are SEPARATE static symbols:
20 *
21 * ssh_pool[MAX_SSH_CONNS] - packet assembly buffers, protocol state
22 * ssh_keys[MAX_SSH_CONNS] - AES-256 key schedules + HMAC keys
23 * ssh_dh[MAX_SSH_CONNS] - ephemeral DH scalar (y), server public (f), K
24 *
25 * The linker places these as independent objects. An overflow inside
26 * pkt_buf must cross the entire ssh_pool[] symbol, then any objects the
27 * linker placed between it and ssh_keys[], before reaching key material.
28 * On ESP32 with the default linker script this is a different RAM region
29 * than a linear overflow from pkt_buf would reach.
30 *
31 * An attacker relying on a single linear write cannot bridge both gaps in
32 * one step. This is not mitigation-by-obscurity - it is the same "separate
33 * key store" principle used by HSMs, but implemented in software via linker
34 * symbol separation.
35 *
36 * DEFENSE 2 - RSA host private key is NEVER stored in static memory
37 * ──────────────────────────────────────────────────────────────────
38 * The RSA-2048 private key (d, p, q, dp, dq, qInv) is loaded from NVS
39 * (encrypted flash on ESP32) into a LOCAL STACK FRAME inside
40 * ssh_rsa_sign(). It is explicitly zeroed (via volatile memset, which
41 * the compiler cannot elide) before ssh_rsa_sign() returns.
42 *
43 * Consequences:
44 * - A static memory scan never finds the private key.
45 * - Cold-boot attacks recover only the public key from BSS.
46 * - The exposure window is the duration of a single mbedtls_rsa_pkcs1_sign
47 * call, typically < 1 ms.
48 *
49 * DEFENSE 3 - DH ephemeral scalar zeroing
50 * ─────────────────────────────────────────
51 * y (2048-bit private DH scalar) lives in ssh_dh[slot].y. After
52 * ssh_dh_finish() derives K it calls ssh_wipe() on the entire SshDhState
53 * struct, which uses a volatile loop to zero all 801 bytes including y,
54 * f, and K. K must be zeroed after session keys are derived from it
55 * (RFC 4253 §7.2 makes no requirement, but it reduces long-term exposure).
56 *
57 * DEFENSE 4 - crypto_work scratch buffer zeroing
58 * ────────────────────────────────────────────────
59 * crypto_work[SSH_CRYPTO_WORK_SIZE] is used for Montgomery multiplication
60 * temporaries (up to 516 bytes of intermediate products that contain
61 * combinations of y, K, and d fragments). It is zeroed via ssh_wipe()
62 * immediately after every call to bn_expmod_group14() or ssh_rsa_sign().
63 *
64 * DEFENSE 5 - MAC-verify-before-use (all packet input)
65 * ──────────────────────────────────────────────────────
66 * After key exchange, every inbound SSH binary packet is:
67 * 1. Received into pkt_buf.
68 * 2. Decrypted in place (AES-256-CTR).
69 * 3. HMAC-SHA2-256 verified over (seq_num_be32 || plaintext_packet).
70 * 4. ONLY THEN forwarded to the protocol handler.
71 *
72 * If HMAC verification fails the connection is closed immediately. No
73 * plaintext bytes are acted upon before the MAC is confirmed valid.
74 * This prevents padding oracle and chosen-ciphertext attacks.
75 *
76 * DEFENSE 6 - Sequence number overflow guard
77 * ────────────────────────────────────────────
78 * RFC 4253 §9.3.4 requires rekeying before the sequence number wraps.
79 * If seq_c2s or seq_s2c reaches 0xFFFFFFFF the connection is closed.
80 * (Rekeying is not yet implemented; close-on-wrap is the safe fallback.)
81 *
82 * WHAT THIS DOES NOT PROTECT AGAINST
83 * ────────────────────────────────────
84 * - An attacker with arbitrary-read in the process can still read
85 * ssh_keys[] - physical separation only raises the bar, not a hard wall.
86 * - Timing side-channels in the software Montgomery/AES paths are not
87 * addressed. On ESP32 the hardware AES path (mbedtls) has
88 * implementation-level timing properties that are mbedtls's concern.
89 * - Cold-boot attacks on SRAM after power-loss are partially mitigated by
90 * the zeroing policies above, but ESP32 SRAM may retain data briefly.
91 *
92 * ═══════════════════════════════════════════════════════════════════════════
93 */
94
95#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_KEYMAT_H
96#define DETERMINISTICESPASYNCWEBSERVER_SSH_KEYMAT_H
97
98#include "ServerConfig.h"
103#include <stddef.h>
104#include <stdint.h>
105#include <string.h>
106
107// These two intentionally stay anonymous *plain* enums - not enum class, not a namespacing struct.
108// The KEX negotiator templates AlgCand<E> / negotiate_alg<E> on decltype(SSH_CIPHER_AES256CTR), so it
109// needs a distinct type per family (cipher vs MAC) for type-safe candidate lists; yet the negotiated
110// value also flows through many uint8_t params/fields (ssh_dh_derive_keys_sid, ssh_mac_is_etm,
111// ssh_mac_len, cipher_alg / mac_mode). A plain enum uniquely gives BOTH - a distinct decltype type and
112// an implicit uint8_t conversion - so it is cast-free. enum class would force a cast at every uint8_t
113// boundary; a struct of static constexpr would collapse decltype to uint8_t and lose the distinction.
114
115/** @brief Negotiated bulk cipher for a session. */
116enum
117{
118 SSH_CIPHER_AES256CTR = 0, ///< aes256-ctr + a separate HMAC (the fallback)
119 SSH_CIPHER_CHACHA20POLY1305 = 1, ///< chacha20-poly1305@openssh.com (AEAD; no separate MAC)
120 SSH_CIPHER_AES256GCM = 2, ///< aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
121};
122
123/** @brief Negotiated MAC for the aes256-ctr cipher (unused with the chacha AEAD). */
124enum
125{
126 SSH_MAC_HMAC_SHA256 = 0, ///< hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
127 SSH_MAC_HMAC_SHA512 = 1, ///< hmac-sha2-512 (encrypt-and-MAC)
128 SSH_MAC_HMAC_SHA256_ETM = 2, ///< hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
129 SSH_MAC_HMAC_SHA512_ETM = 3, ///< hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
130};
131
132/** @brief True if @p mac_mode is an encrypt-then-MAC variant (length in the clear, MAC over ciphertext). */
133static inline bool ssh_mac_is_etm(uint8_t mac_mode)
134{
135 return mac_mode == SSH_MAC_HMAC_SHA256_ETM || mac_mode == SSH_MAC_HMAC_SHA512_ETM;
136}
137/** @brief MAC tag / key length in bytes for @p mac_mode (32 for SHA-256, 64 for SHA-512). */
138static inline uint8_t ssh_mac_len(uint8_t mac_mode)
139{
140 return (mac_mode == SSH_MAC_HMAC_SHA512 || mac_mode == SSH_MAC_HMAC_SHA512_ETM) ? 64 : 32;
141}
142
143// ---------------------------------------------------------------------------
144// Secure wipe
145// ---------------------------------------------------------------------------
146
147/**
148 * @brief Zero @p len bytes at @p ptr using a volatile loop the compiler
149 * cannot optimize away.
150 *
151 * Use this (not memset) for any buffer that contains key material.
152 * C compilers are permitted to elide a plain memset() whose result is
153 * "not observed" - a common optimization that silently skips zeroing of
154 * local key buffers before they go out of scope. The volatile write
155 * forces the store to happen even if the memory is never read again.
156 *
157 * @param ptr Buffer to wipe.
158 * @param len Number of bytes to zero.
159 */
160static inline void ssh_wipe(void *ptr, size_t len)
161{
162 volatile uint8_t *p = (volatile uint8_t *)ptr;
163 for (size_t i = 0; i < len; i++)
164 p[i] = 0;
165}
166
167// ---------------------------------------------------------------------------
168// Session key material (one entry per SSH connection)
169// ---------------------------------------------------------------------------
170
171/**
172 * @brief AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
173 *
174 * This struct occupies a separate BSS symbol (ssh_keys[]) from the packet
175 * receive buffer (ssh_pool[].pkt_buf). See the security model at the top
176 * of this file for why that separation matters.
177 *
178 * Key derivation follows RFC 4253 §7.2. After the DH exchange hash H is
179 * known and K is available, six values are derived:
180 *
181 * IV_c2s = SHA256(K || H || "A" || session_id) [16 bytes]
182 * IV_s2c = SHA256(K || H || "B" || session_id) [16 bytes]
183 * key_c2s = SHA256(K || H || "C" || session_id) [32 bytes]
184 * key_s2c = SHA256(K || H || "D" || session_id) [32 bytes]
185 * mac_c2s = SHA256(K || H || "E" || session_id) [32 bytes]
186 * mac_s2c = SHA256(K || H || "F" || session_id) [32 bytes]
187 *
188 * c2s_ctx is initialized with key_c2s + IV_c2s (client-to-server, server decrypts).
189 * s2c_ctx is initialized with key_s2c + IV_s2c (server-to-client, server encrypts).
190 */
192{
193 SshAesCtrCtx c2s_ctx; ///< Client→server cipher (AES-256-CTR); server decrypts inbound with it.
194 SshAesCtrCtx s2c_ctx; ///< Server→client cipher (AES-256-CTR); server encrypts outbound with it.
195
196 uint8_t mac_key_c2s[64]; ///< HMAC key, client-to-server (aes mode); 32 bytes for SHA-256, 64 for SHA-512.
197 uint8_t mac_key_s2c[64]; ///< HMAC key, server-to-client (aes mode).
198 uint8_t mac_mode; ///< SSH_MAC_* selected for the aes256-ctr cipher (0 = hmac-sha2-256 E&M).
199
200 uint8_t cipher_mode; ///< SSH_CIPHER_* selected for this session (0 = aes256-ctr).
201 // chacha20-poly1305@openssh.com: 512-bit key per direction (K_main || K_header); no IV, no MAC key.
202 uint8_t chacha_key_c2s[SSH_CHACHAPOLY_KEY_LEN]; ///< client-to-server, used only in chacha mode.
203 uint8_t chacha_key_s2c[SSH_CHACHAPOLY_KEY_LEN]; ///< server-to-client, used only in chacha mode.
204
205 // aes256-gcm@openssh.com (RFC 5647): a stateful AEAD context per direction (256-bit key + 96-bit
206 // nonce whose invocation counter advances per packet); no separate MAC key. Used only in gcm mode.
207 SshAesGcmCtx gcm_c2s; ///< client-to-server AES-256-GCM (server opens inbound with it).
208 SshAesGcmCtx gcm_s2c; ///< server-to-client AES-256-GCM (server seals outbound with it).
209
210 bool active; ///< True once keys are installed after successful KEX.
211};
212
213/**
214 * @brief Pool of session key material, one entry per MAX_SSH_CONNS.
215 *
216 * Separate BSS symbol from ssh_pool[] - see security model.
217 * Zeroed on connection close by ssh_keymat_wipe(slot).
218 */
220
221// ---------------------------------------------------------------------------
222// DH ephemeral state (one entry per SSH connection, zeroed after KEX)
223// ---------------------------------------------------------------------------
224
225/**
226 * @brief Ephemeral Diffie-Hellman state for one SSH connection.
227 *
228 * The three SshBigNum fields (y, f, K) together hold 768 bytes of sensitive
229 * material. The entire struct is wiped by ssh_dh_wipe() immediately after
230 * session keys are derived from K.
231 *
232 * FIELD LIFETIME:
233 * y - generated by ssh_dh_generate(); zeroed in ssh_dh_wipe().
234 * f - computed in ssh_dh_generate() as g^y mod p; sent in KEXDH_REPLY;
235 * zeroed in ssh_dh_wipe().
236 * K - computed in ssh_dh_finish() as e^y mod p; used for key derivation;
237 * zeroed in ssh_dh_wipe() AFTER keys are installed.
238 * H - the exchange hash; becomes the session_id for the connection's
239 * lifetime (RFC 4253 §7.2); stored in H[], NOT zeroed (it is a
240 * commitment to the handshake, and is not secret).
241 */
243{
244 SshBigNum y; ///< Server ephemeral private DH scalar (SENSITIVE - wiped after KEX).
245 SshBigNum f; ///< Server DH public value = g^y mod p (sent to client).
246 SshBigNum K; ///< Shared DH secret = e^y mod p (SENSITIVE - wiped after key derivation).
247
248 uint8_t H[32]; ///< SHA-256 exchange hash; doubles as session_id after first KEX.
249 bool kex_done; ///< True once NEWKEYS has been sent and received.
250};
251
252/** @brief Pool of ephemeral DH state, one entry per MAX_SSH_CONNS. */
254
255// ---------------------------------------------------------------------------
256// Wipe helpers
257// ---------------------------------------------------------------------------
258
259/** @brief Zero all key material for slot @p i on disconnect or KEX failure. */
260static inline void ssh_keymat_wipe(uint8_t i)
261{
262 if (i < MAX_SSH_CONNS)
263 ssh_wipe(&ssh_keys[i], sizeof(SshKeyMat));
264}
265
266/** @brief Zero the ephemeral DH state for slot @p i after keys are derived. */
267static inline void ssh_dh_wipe(uint8_t i)
268{
269 if (i < MAX_SSH_CONNS)
270 ssh_wipe(&ssh_dh[i], sizeof(SshDhState));
271}
272
273#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_KEYMAT_H
User-facing configuration for DeterministicESPAsyncWebServer.
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
AES-256-CTR stream cipher context and API.
AES-256-GCM AEAD for SSH (aes256-gcm@openssh.com, RFC 5647).
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
chacha20-poly1305@openssh.com AEAD cipher (OpenSSH PROTOCOL.chacha20poly1305).
#define SSH_CHACHAPOLY_KEY_LEN
two 256-bit ChaCha20 keys
@ SSH_CIPHER_CHACHA20POLY1305
chacha20-poly1305@openssh.com (AEAD; no separate MAC)
Definition ssh_keymat.h:119
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
Definition ssh_keymat.h:120
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
Definition ssh_keymat.h:118
SshDhState ssh_dh[MAX_SSH_CONNS]
Pool of ephemeral DH state, one entry per MAX_SSH_CONNS.
SshKeyMat ssh_keys[MAX_SSH_CONNS]
Pool of session key material, one entry per MAX_SSH_CONNS.
@ SSH_MAC_HMAC_SHA256_ETM
hmac-sha2-256-etm@openssh.com (encrypt-then-MAC)
Definition ssh_keymat.h:128
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
Definition ssh_keymat.h:126
@ SSH_MAC_HMAC_SHA512
hmac-sha2-512 (encrypt-and-MAC)
Definition ssh_keymat.h:127
@ SSH_MAC_HMAC_SHA512_ETM
hmac-sha2-512-etm@openssh.com (encrypt-then-MAC)
Definition ssh_keymat.h:129
AES-256-GCM context for one SSH direction (HW AES on ESP32).
Definition ssh_aesgcm.h:50
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition ssh_bignum.h:96
Ephemeral Diffie-Hellman state for one SSH connection.
Definition ssh_keymat.h:243
SshBigNum K
Shared DH secret = e^y mod p (SENSITIVE - wiped after key derivation).
Definition ssh_keymat.h:246
SshBigNum f
Server DH public value = g^y mod p (sent to client).
Definition ssh_keymat.h:245
SshBigNum y
Server ephemeral private DH scalar (SENSITIVE - wiped after KEX).
Definition ssh_keymat.h:244
uint8_t H[32]
SHA-256 exchange hash; doubles as session_id after first KEX.
Definition ssh_keymat.h:248
bool kex_done
True once NEWKEYS has been sent and received.
Definition ssh_keymat.h:249
AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
Definition ssh_keymat.h:192
SshAesGcmCtx gcm_s2c
server-to-client AES-256-GCM (server seals outbound with it).
Definition ssh_keymat.h:208
uint8_t mac_key_c2s[64]
HMAC key, client-to-server (aes mode); 32 bytes for SHA-256, 64 for SHA-512.
Definition ssh_keymat.h:196
SshAesCtrCtx c2s_ctx
Client→server cipher (AES-256-CTR); server decrypts inbound with it.
Definition ssh_keymat.h:193
SshAesGcmCtx gcm_c2s
client-to-server AES-256-GCM (server opens inbound with it).
Definition ssh_keymat.h:207
uint8_t cipher_mode
SSH_CIPHER_* selected for this session (0 = aes256-ctr).
Definition ssh_keymat.h:200
uint8_t mac_key_s2c[64]
HMAC key, server-to-client (aes mode).
Definition ssh_keymat.h:197
bool active
True once keys are installed after successful KEX.
Definition ssh_keymat.h:210
uint8_t chacha_key_c2s[SSH_CHACHAPOLY_KEY_LEN]
client-to-server, used only in chacha mode.
Definition ssh_keymat.h:202
uint8_t chacha_key_s2c[SSH_CHACHAPOLY_KEY_LEN]
server-to-client, used only in chacha mode.
Definition ssh_keymat.h:203
uint8_t mac_mode
SSH_MAC_* selected for the aes256-ctr cipher (0 = hmac-sha2-256 E&M).
Definition ssh_keymat.h:198
SshAesCtrCtx s2c_ctx
Server→client cipher (AES-256-CTR); server encrypts outbound with it.
Definition ssh_keymat.h:194