DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_dh.cpp
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_dh.cpp
6 * @brief DH-group14-SHA256 key exchange implementation.
7 */
8
11#include <Arduino.h> // for esp_random() / esp_fill_random() (real or mock)
12#include <string.h>
13
14// ---------------------------------------------------------------------------
15// RNG
16// ---------------------------------------------------------------------------
17
18void ssh_rng_fill(uint8_t *buf, size_t len)
19{
20 esp_fill_random(buf, len);
21}
22
23// ---------------------------------------------------------------------------
24// DH key generation
25// ---------------------------------------------------------------------------
26
27int ssh_dh_generate(uint8_t i)
28{
29 if (i >= MAX_SSH_CONNS)
30 return -1;
31 SshDhState *dh = &ssh_dh[i];
32
33 // Generate a random 2048-bit private scalar y.
34 // RFC 4253 §8 does not specify a minimum bit-length for y beyond requiring
35 // it to be in [1, p-1]. Common practice is a full 2048-bit random value,
36 // which ensures the discrete-log is as hard as the group order.
37 ssh_rng_fill((uint8_t *)dh->y.d, sizeof(SshBigNum));
38
39 // Ensure y < p by clearing the two MSBs (conservative; not strictly
40 // required since rejection sampling would also work, but a single mask
41 // is sufficient because p ≈ 2^2048 and clearing 2 bits keeps y in range).
42 dh->y.d[SSH_BN_LIMBS - 1] &= 0x3FFFFFFFu;
43 // Also ensure y > 1 (set bit 1 of the LSB limb to avoid pathological y=0,1).
44 dh->y.d[0] |= 0x00000002u;
45
46 // f = g^y mod p (g = 2 for group-14)
47 bn_expmod_group14(&dh->f, &group14_g, &dh->y);
48
49 dh->kex_done = false;
50 return 0;
51}
52
53// ---------------------------------------------------------------------------
54// Session key derivation (RFC 4253 §7.2)
55// ---------------------------------------------------------------------------
56
57// Hash the shared secret K as an SSH mpint into @p ctx (RFC 4251 §5 / RFC 4253
58// §7.2): big-endian, 4-byte length prefix, a leading 0x00 only if the MSB is set,
59// and all UNNECESSARY leading 0x00 bytes stripped (canonical form). The exchange
60// hash encodes K the same way (hash_mpint), so the KDF must too: if K has any
61// high-order zero bytes (~1/256 of handshakes) a spec-compliant peer strips them
62// and would otherwise derive different keys.
63static void hash_mpint_K(SshSha256Ctx *ctx, const uint8_t K_be[256])
64{
65 size_t off = 0;
66 while (off < 256 && K_be[off] == 0x00u)
67 off++;
68 if (off == 256) // K == 0: empty mpint (not reachable for a real DH secret)
69 {
70 uint8_t len_be[4] = {0, 0, 0, 0};
71 ssh_sha256_update(ctx, len_be, 4);
72 return;
73 }
74 bool pad = (K_be[off] & 0x80u) != 0;
75 uint32_t mlen = (uint32_t)(256 - off) + (pad ? 1u : 0u);
76 uint8_t len_be[4] = {(uint8_t)(mlen >> 24), (uint8_t)(mlen >> 16), (uint8_t)(mlen >> 8), (uint8_t)mlen};
77 ssh_sha256_update(ctx, len_be, 4);
78 if (pad)
79 {
80 uint8_t zero = 0x00u;
81 ssh_sha256_update(ctx, &zero, 1);
82 }
83 ssh_sha256_update(ctx, K_be + off, 256 - off);
84}
85
86// Hybrid KEX (mlkem768x25519-sha256): K is a fixed 32-octet HASH output, hashed as a plain SSH
87// string (RFC 4251 §5) - length prefix then the bytes verbatim, NO mpint sign/strip. It lives in the
88// last 32 octets of the right-aligned K_be buffer. Both H and this KDF must encode K identically.
89static void hash_string_K(SshSha256Ctx *ctx, const uint8_t K_be[256])
90{
91 uint8_t len_be[4] = {0, 0, 0, 32};
92 ssh_sha256_update(ctx, len_be, 4);
93 ssh_sha256_update(ctx, K_be + (256 - 32), 32);
94}
95
96static inline void hash_K(SshSha256Ctx *ctx, const uint8_t K_be[256], bool k_is_string)
97{
98 if (k_is_string)
99 hash_string_K(ctx, K_be);
100 else
101 hash_mpint_K(ctx, K_be);
102}
103
104// RFC 4253 §7.2 key derivation extended to any length:
105// K1 = HASH(mpint(K) || H || X || session_id) (X = label byte)
106// K2 = HASH(mpint(K) || H || K1)
107// K3 = HASH(mpint(K) || H || K1 || K2) ... key = K1 || K2 || K3 || ...
108// @p out_len up to SSH_KDF_MAX. For the first KEX session_id == H; on a re-key it
109// is the H from the first KEX.
110void ssh_kdf_derive(const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN],
111 const uint8_t session_id[SSH_SHA256_DIGEST_LEN], char label, uint8_t *out, size_t out_len,
112 bool k_is_string)
113{
114 if (out_len > SSH_KDF_MAX)
115 out_len = SSH_KDF_MAX; // bounded: every negotiated algorithm needs <= 32 B today
116 uint8_t acc[SSH_KDF_MAX]; // K1 || K2 || ... accumulated for the chain hash
117 size_t have = 0;
118
119 // K1 = HASH(K || H || label || session_id) (K encoded per the KEX method: mpint or string)
120 SshSha256Ctx ctx;
121 ssh_sha256_init(&ctx);
122 hash_K(&ctx, K_be, k_is_string);
124 uint8_t lbl = (uint8_t)label;
125 ssh_sha256_update(&ctx, &lbl, 1);
126 ssh_sha256_update(&ctx, session_id, SSH_SHA256_DIGEST_LEN);
127 ssh_sha256_final(&ctx, acc); // acc[0..31] = K1
129
130 // Ki+1 = HASH(K || H || K1..Ki) until enough material.
131 while (have < out_len && have + SSH_SHA256_DIGEST_LEN <= SSH_KDF_MAX)
132 {
133 ssh_sha256_init(&ctx);
134 hash_K(&ctx, K_be, k_is_string);
136 ssh_sha256_update(&ctx, acc, have); // all prior blocks
137 ssh_sha256_final(&ctx, acc + have);
138 have += SSH_SHA256_DIGEST_LEN;
139 }
140 memcpy(out, acc, out_len);
141}
142
143// One 32-byte derived value (the only size any negotiated algorithm needs today).
144static void derive_key(const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN],
145 const uint8_t session_id[SSH_SHA256_DIGEST_LEN], char label, uint8_t out[SSH_SHA256_DIGEST_LEN],
146 bool k_is_string)
147{
148 ssh_kdf_derive(K_be, H, session_id, label, out, SSH_SHA256_DIGEST_LEN, k_is_string);
149}
150
151void ssh_dh_derive_keys_sid(uint8_t i, const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN],
152 const uint8_t session_id[SSH_SHA256_DIGEST_LEN], uint8_t cipher_alg, uint8_t mac_alg,
153 bool k_is_string)
154{
155 if (i >= MAX_SSH_CONNS)
156 return;
157 SshKeyMat *km = &ssh_keys[i];
158 km->cipher_mode = cipher_alg;
159 km->mac_mode = mac_alg;
160
161 if (cipher_alg == SSH_CIPHER_CHACHA20POLY1305)
162 {
163 // chacha20-poly1305@openssh.com: a 512-bit key per direction (labels 'C'/'D'), no IV and
164 // no separate MAC key (the AEAD authenticates). The 64 bytes come from the RFC 4253 §7.2
165 // extension chain (K1 || K2).
166 ssh_kdf_derive(K_be, H, session_id, 'C', km->chacha_key_c2s, SSH_CHACHAPOLY_KEY_LEN, k_is_string);
167 ssh_kdf_derive(K_be, H, session_id, 'D', km->chacha_key_s2c, SSH_CHACHAPOLY_KEY_LEN, k_is_string);
168 km->active = true;
169 return;
170 }
171
172 if (cipher_alg == SSH_CIPHER_AES256GCM)
173 {
174 // aes256-gcm@openssh.com (RFC 5647): a 256-bit key (labels 'C'/'D') and a 96-bit initial IV
175 // (the first 12 bytes of the 'A'/'B' IV material) per direction; no separate MAC key (AEAD).
176 uint8_t iv_c2s[SSH_SHA256_DIGEST_LEN];
177 uint8_t iv_s2c[SSH_SHA256_DIGEST_LEN];
178 uint8_t key_c2s[32];
179 uint8_t key_s2c[32];
180 derive_key(K_be, H, session_id, 'A', iv_c2s, k_is_string); // IV C→S (first 12 bytes used)
181 derive_key(K_be, H, session_id, 'B', iv_s2c, k_is_string); // IV S→C
182 derive_key(K_be, H, session_id, 'C', key_c2s, k_is_string); // key C→S
183 derive_key(K_be, H, session_id, 'D', key_s2c, k_is_string); // key S→C
184 ssh_aesgcm_init(&km->gcm_c2s, key_c2s, iv_c2s);
185 ssh_aesgcm_init(&km->gcm_s2c, key_s2c, iv_s2c);
186 ssh_wipe(key_c2s, sizeof(key_c2s));
187 ssh_wipe(key_s2c, sizeof(key_s2c));
188 ssh_wipe(iv_c2s, sizeof(iv_c2s));
189 ssh_wipe(iv_s2c, sizeof(iv_s2c));
190 km->active = true;
191 return;
192 }
193
194 // aes256-ctr + HMAC-SHA2-256: RFC 4253 §7.2 derives six values, each keyed by a label 'A'..'F'.
195 // The AES contexts need both key and IV at init time, so derive all six values first.
196 uint8_t iv_c2s[SSH_SHA256_DIGEST_LEN];
197 uint8_t iv_s2c[SSH_SHA256_DIGEST_LEN];
198 uint8_t key_c2s[32];
199 uint8_t key_s2c[32];
200
201 derive_key(K_be, H, session_id, 'A', iv_c2s, k_is_string); // IV C→S (first 16 bytes used)
202 derive_key(K_be, H, session_id, 'B', iv_s2c, k_is_string); // IV S→C
203 derive_key(K_be, H, session_id, 'C', key_c2s, k_is_string); // cipher key C→S
204 derive_key(K_be, H, session_id, 'D', key_s2c, k_is_string); // cipher key S→C
205 uint8_t mlen = ssh_mac_len(mac_alg); // 32 (SHA-256) or 64 (SHA-512)
206 ssh_kdf_derive(K_be, H, session_id, 'E', km->mac_key_c2s, mlen, k_is_string); // MAC key C→S
207 ssh_kdf_derive(K_be, H, session_id, 'F', km->mac_key_s2c, mlen, k_is_string); // MAC key S→C
208
209 ssh_aes256ctr_init(&km->c2s_ctx, key_c2s, iv_c2s);
210 ssh_aes256ctr_init(&km->s2c_ctx, key_s2c, iv_s2c);
211
212 // Wipe stack temporaries (key material).
213 ssh_wipe(key_c2s, sizeof(key_c2s));
214 ssh_wipe(key_s2c, sizeof(key_s2c));
215 ssh_wipe(iv_c2s, sizeof(iv_c2s));
216 ssh_wipe(iv_s2c, sizeof(iv_s2c));
217
218 km->active = true;
219}
220
221void ssh_dh_derive_keys(uint8_t i, const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN])
222{
223 // First-KEX convenience: session id equals H; aes256-ctr + hmac-sha2-256 (pre-negotiation defaults).
225}
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
void ssh_aes256ctr_init(SshAesCtrCtx *ctx, const uint8_t key[32], const uint8_t iv[16])
Initialize an AES-256-CTR context.
void ssh_aesgcm_init(SshAesGcmCtx *ctx, const uint8_t key[SSH_AESGCM_KEY_LEN], const uint8_t iv[SSH_AESGCM_IV_LEN])
Initialize an AES-256-GCM context: expand the key, precompute H, latch the initial nonce.
void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp)
Compute out = base^exp mod group14_p.
const SshBigNum group14_g
Generator for group-14: g = 2.
#define SSH_BN_LIMBS
Number of 32-bit limbs in a 2048-bit integer.
Definition ssh_bignum.h:87
#define SSH_CHACHAPOLY_KEY_LEN
two 256-bit ChaCha20 keys
void ssh_dh_derive_keys(uint8_t i, const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN])
Derive the six session keys from shared secret K and exchange hash H.
Definition ssh_dh.cpp:221
void ssh_dh_derive_keys_sid(uint8_t i, const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN], const uint8_t session_id[SSH_SHA256_DIGEST_LEN], uint8_t cipher_alg, uint8_t mac_alg, bool k_is_string)
Derive session keys with an explicit session id (RFC 4253 §7.2).
Definition ssh_dh.cpp:151
void ssh_rng_fill(uint8_t *buf, size_t len)
Fill len bytes of buf with cryptographically random data.
Definition ssh_dh.cpp:18
void ssh_kdf_derive(const uint8_t K_be[256], const uint8_t H[SSH_SHA256_DIGEST_LEN], const uint8_t session_id[SSH_SHA256_DIGEST_LEN], char label, uint8_t *out, size_t out_len, bool k_is_string)
RFC 4253 §7.2 key derivation for any length up to SSH_KDF_MAX.
Definition ssh_dh.cpp:110
int ssh_dh_generate(uint8_t i)
Generate the server ephemeral DH key pair for connection slot i.
Definition ssh_dh.cpp:27
DH-group14-SHA256 key exchange (RFC 4253 §8 + RFC 8268).
#define SSH_KDF_MAX
Max bytes ssh_kdf_derive() can produce (4 SHA-256 blocks).
Definition ssh_dh.h:129
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
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_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
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
Definition ssh_keymat.h:126
void ssh_sha256_init(SshSha256Ctx *ctx)
Initialize a streaming SHA-256 context.
void ssh_sha256_update(SshSha256Ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
void ssh_sha256_final(SshSha256Ctx *ctx, uint8_t digest[SSH_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition ssh_bignum.h:96
uint32_t d[SSH_BN_LIMBS]
256 bytes of magnitude, little-endian limbs.
Definition ssh_bignum.h:97
Ephemeral Diffie-Hellman state for one SSH connection.
Definition ssh_keymat.h:243
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
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
Streaming SHA-256 context.
Definition ssh_sha256.h:44