ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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
12#include "server/mmgr/secure.h"
13#include <Arduino.h> // for esp_random() / esp_fill_random() (real or mock)
14#include <string.h>
15
16// ---------------------------------------------------------------------------
17// RNG
18// ---------------------------------------------------------------------------
19
20void ssh_rng_fill(uint8_t *buf, size_t len)
21{
22 esp_fill_random(buf, len);
23}
24
25// ---------------------------------------------------------------------------
26// DH key generation
27// ---------------------------------------------------------------------------
28
29int ssh_dh_generate(uint8_t i)
30{
31 if (i >= MAX_SSH_CONNS)
32 {
33 return -1;
34 }
35 SshDhState *dh = &ssh_dh[i];
36
37 // Generate a random 2048-bit private scalar y.
38 // RFC 4253 §8 does not specify a minimum bit-length for y beyond requiring
39 // it to be in [1, p-1]. Common practice is a full 2048-bit random value,
40 // which ensures the discrete-log is as hard as the group order.
41 ssh_rng_fill((uint8_t *)dh->y.d, sizeof(pc_bignum));
42
43 // Ensure y < p by clearing the two MSBs (conservative; not strictly
44 // required since rejection sampling would also work, but a single mask
45 // is sufficient because p ≈ 2^2048 and clearing 2 bits keeps y in range).
46 dh->y.d[PC_BN_LIMBS - 1] &= 0x3FFFFFFFu;
47 // Also ensure y > 1 (set bit 1 of the LSB limb to avoid pathological y=0,1).
48 dh->y.d[0] |= 0x00000002u;
49
50 // f = g^y mod p (g = 2 for group-14)
51 bn_expmod_group14(&dh->f, &group14_g, &dh->y);
52
53 dh->kex_done = false;
54 return 0;
55}
56
57// ---------------------------------------------------------------------------
58// Session key derivation (RFC 4253 §7.2)
59// ---------------------------------------------------------------------------
60
61// Hash the shared secret K as an SSH mpint into @p ctx (RFC 4251 §5 / RFC 4253
62// §7.2): big-endian, 4-byte length prefix, a leading 0x00 only if the MSB is set,
63// and all UNNECESSARY leading 0x00 bytes stripped (canonical form). The exchange
64// hash encodes K the same way (hash_mpint), so the KDF must too: if K has any
65// high-order zero bytes (~1/256 of handshakes) a spec-compliant peer strips them
66// and would otherwise derive different keys.
67static void hash_mpint_K(SshKexHash *h, const uint8_t K_be[256])
68{
69 size_t off = 0;
70 while (off < 256 && K_be[off] == 0x00u)
71 {
72 off++;
73 }
74 if (off == 256) // K == 0: empty mpint (not reachable for a real DH secret)
75 {
76 uint8_t len_be[4] = {0, 0, 0, 0};
77 ssh_kexhash_update(h, len_be, 4);
78 return;
79 }
80 bool pad = (K_be[off] & 0x80u) != 0;
81 uint32_t mlen = (uint32_t)(256 - off) + (pad ? 1u : 0u);
82 uint8_t len_be[4] = {(uint8_t)(mlen >> 24), (uint8_t)(mlen >> 16), (uint8_t)(mlen >> 8), (uint8_t)mlen};
83 ssh_kexhash_update(h, len_be, 4);
84 if (pad)
85 {
86 uint8_t zero = 0x00u;
87 ssh_kexhash_update(h, &zero, 1);
88 }
89 ssh_kexhash_update(h, K_be + off, 256 - off);
90}
91
92// Hybrid KEX: K is a fixed HASH output (32 for mlkem-sha256, 64 for sntrup761-sha512), hashed as a
93// plain SSH string (RFC 4251 §5) - length prefix then the bytes verbatim, NO mpint sign/strip. It
94// lives in the last @p klen octets of the right-aligned K_be buffer. H and this KDF encode K the same.
95static void hash_string_K(SshKexHash *h, const uint8_t K_be[256], size_t klen)
96{
97 uint8_t len_be[4] = {(uint8_t)(klen >> 24), (uint8_t)(klen >> 16), (uint8_t)(klen >> 8), (uint8_t)klen};
98 ssh_kexhash_update(h, len_be, 4);
99 ssh_kexhash_update(h, K_be + (256 - klen), klen);
100}
101
102static inline void hash_K(SshKexHash *h, const uint8_t K_be[256], bool k_is_string, size_t k_str_len)
103{
104 if (k_is_string)
105 {
106 hash_string_K(h, K_be, k_str_len);
107 }
108 else
109 {
110 hash_mpint_K(h, K_be);
111 }
112}
113
114// RFC 4253 §7.2 key derivation extended to any length, over the KEX method's hash (SHA-256 or
115// SHA-512 via SshKexHash / @p is512):
116// K1 = HASH(K || H || X || session_id) (X = label byte); Ki+1 = HASH(K || H || K1..Ki)
117// key = K1 || K2 || ... For the first KEX session_id == H; on a re-key it is the first KEX's H.
118// @p h_len / @p sid_len are the exchange-hash / session-id lengths. When K is a hybrid string it is
119// @p k_str_len octets (the KEX hash length). @p out_len up to SSH_KDF_MAX.
120void ssh_kdf_derive(const uint8_t K_be[256], const uint8_t *H, const uint8_t *session_id, char label, uint8_t *out,
121 size_t out_len, bool k_is_string, size_t h_len, size_t sid_len, bool is512)
122{
123 const size_t blk = ssh_kexhash_len(is512); // 32 or 64
124 const size_t k_str_len = ssh_kexhash_len(is512);
125 if (out_len > SSH_KDF_MAX)
126 {
127 out_len = SSH_KDF_MAX; // bounded: every negotiated algorithm needs <= 64 B today
128 }
129 uint8_t acc[SSH_KDF_MAX]; // K1 || K2 || ... accumulated for the chain hash
130 size_t have = 0;
131
132 SshKexHash h;
133 ssh_kexhash_init(&h, is512);
134 hash_K(&h, K_be, k_is_string, k_str_len);
135 ssh_kexhash_update(&h, H, h_len);
136 uint8_t lbl = (uint8_t)label;
137 ssh_kexhash_update(&h, &lbl, 1);
138 ssh_kexhash_update(&h, session_id, sid_len);
139 ssh_kexhash_final(&h, acc); // acc[0..blk-1] = K1
140 have = blk;
141
142 // have + blk > SSH_KDF_MAX (loop exit via the right operand) is unreachable: blk is only ever 32 or
143 // 64 (ssh_kexhash_len(is512)) and SSH_KDF_MAX is 128, an exact multiple of both; out_len is already
144 // clamped to <= SSH_KDF_MAX above, and have only grows in whole increments of blk starting at blk -
145 // so whenever have < out_len (<= SSH_KDF_MAX) it is at most SSH_KDF_MAX - blk, and this half is
146 // always true.
147 while (have < out_len &&
148 have + blk <= SSH_KDF_MAX) // GCOVR_EXCL_BR_LINE have+blk never exceeds SSH_KDF_MAX, see above
149 {
150 ssh_kexhash_init(&h, is512);
151 hash_K(&h, K_be, k_is_string, k_str_len);
152 ssh_kexhash_update(&h, H, h_len);
153 ssh_kexhash_update(&h, acc, have); // all prior blocks
154 ssh_kexhash_final(&h, acc + have);
155 have += blk;
156 }
157 memcpy(out, acc, out_len);
158}
159
160// One 32-byte derived value (the only size any negotiated cipher key/IV needs today).
161static void derive_key(const uint8_t K_be[256], const uint8_t *H, const uint8_t *session_id, char label,
162 uint8_t out[PC_SHA256_DIGEST_LEN], bool k_is_string, size_t h_len, size_t sid_len, bool is512)
163{
164 ssh_kdf_derive(K_be, H, session_id, label, out, PC_SHA256_DIGEST_LEN, k_is_string, h_len, sid_len, is512);
165}
166
167void ssh_dh_derive_keys_sid(uint8_t i, const uint8_t K_be[256], const uint8_t *H, const uint8_t *session_id,
168 uint8_t cipher_alg, uint8_t mac_alg, bool k_is_string, size_t h_len, size_t sid_len,
169 bool is512)
170{
171 if (i >= MAX_SSH_CONNS)
172 {
173 return;
174 }
175 SshKeyMat *km = &ssh_keys[i];
176 // Rekey lands here with live contexts still in the slot. Release them before cipher_mode is
177 // overwritten, after which the outgoing mode is no longer knowable.
178 if (km->active && km->cipher_mode == SSH_CIPHER_AES256GCM)
179 {
180 pc_aesgcm_key_wipe(reinterpret_cast<pc_aesgcm_key *>(km->gcm_ctx_c2s));
181 pc_aesgcm_key_wipe(reinterpret_cast<pc_aesgcm_key *>(km->gcm_ctx_s2c));
182 }
183 km->cipher_mode = cipher_alg;
184 km->mac_mode = mac_alg;
185
186 if (cipher_alg == SSH_CIPHER_CHACHA20POLY1305)
187 {
188 // chacha20-poly1305@openssh.com: a 512-bit key per direction (labels 'C'/'D'), no IV and
189 // no separate MAC key (the AEAD authenticates). The 64 bytes come from the RFC 4253 §7.2
190 // extension chain (K1 || K2).
191 ssh_kdf_derive(K_be, H, session_id, 'C', km->chacha_key_c2s, PC_CHACHAPOLY_KEY_LEN, k_is_string, h_len, sid_len,
192 is512);
193 ssh_kdf_derive(K_be, H, session_id, 'D', km->chacha_key_s2c, PC_CHACHAPOLY_KEY_LEN, k_is_string, h_len, sid_len,
194 is512);
195 km->active = true;
196 return;
197 }
198
199 if (cipher_alg == SSH_CIPHER_AES256GCM)
200 {
201 // aes256-gcm@openssh.com (RFC 5647): a 256-bit key (labels 'C'/'D') and a 96-bit initial IV
202 // (the first 12 bytes of the 'A'/'B' IV material) per direction; no separate MAC key (AEAD).
203 uint8_t iv_c2s[PC_SHA256_DIGEST_LEN];
204 uint8_t iv_s2c[PC_SHA256_DIGEST_LEN];
205 uint8_t key_c2s[32];
206 uint8_t key_s2c[32];
207 derive_key(K_be, H, session_id, 'A', iv_c2s, k_is_string, h_len, sid_len, is512); // IV C→S (first 12 used)
208 derive_key(K_be, H, session_id, 'B', iv_s2c, k_is_string, h_len, sid_len, is512); // IV S→C
209 derive_key(K_be, H, session_id, 'C', key_c2s, k_is_string, h_len, sid_len, is512); // key C→S
210 derive_key(K_be, H, session_id, 'D', key_s2c, k_is_string, h_len, sid_len, is512); // key S→C
211 // Build the keyed contexts now and keep the nonce (GCM nonce = low 12 bytes of the 16-byte IV
212 // field). The raw keys are wiped below and never stored: the context holds the schedule, so unlike
213 // CTR mode this slot ends up with no raw GCM key in it at all.
214 pc_aesgcm_key_init(km->gcm_ctx_c2s, key_c2s);
215 pc_aesgcm_key_init(km->gcm_ctx_s2c, key_s2c);
216 memcpy(km->aes_iv_c2s, iv_c2s, sizeof(km->aes_iv_c2s));
217 memcpy(km->aes_iv_s2c, iv_s2c, sizeof(km->aes_iv_s2c));
218 pc_secure_wipe(key_c2s, sizeof(key_c2s));
219 pc_secure_wipe(key_s2c, sizeof(key_s2c));
220 pc_secure_wipe(iv_c2s, sizeof(iv_c2s));
221 pc_secure_wipe(iv_s2c, sizeof(iv_s2c));
222 km->active = true;
223 return;
224 }
225
226 // aes256-ctr + HMAC-SHA2-256: RFC 4253 §7.2 derives six values, each keyed by a label 'A'..'F'.
227 // The AES contexts need both key and IV at init time, so derive all six values first.
228 uint8_t iv_c2s[PC_SHA256_DIGEST_LEN];
229 uint8_t iv_s2c[PC_SHA256_DIGEST_LEN];
230 uint8_t key_c2s[32];
231 uint8_t key_s2c[32];
232
233 derive_key(K_be, H, session_id, 'A', iv_c2s, k_is_string, h_len, sid_len, is512); // IV C→S (first 16 used)
234 derive_key(K_be, H, session_id, 'B', iv_s2c, k_is_string, h_len, sid_len, is512); // IV S→C
235 derive_key(K_be, H, session_id, 'C', key_c2s, k_is_string, h_len, sid_len, is512); // cipher key C→S
236 derive_key(K_be, H, session_id, 'D', key_s2c, k_is_string, h_len, sid_len, is512); // cipher key S→C
237 uint8_t mlen = ssh_mac_len(mac_alg); // 32 (SHA-256) or 64 (SHA-512)
238 ssh_kdf_derive(K_be, H, session_id, 'E', km->mac_key_c2s, mlen, k_is_string, h_len, sid_len, is512); // MAC C→S
239 ssh_kdf_derive(K_be, H, session_id, 'F', km->mac_key_s2c, mlen, k_is_string, h_len, sid_len, is512); // MAC S→C
240
241 // Store only the raw key + the initial counter (first 16 bytes of the derived IV); the key schedule is
242 // rebuilt per packet in the shared crypto scratch, so no expanded key ever lands in the keymat pool.
243 memcpy(km->aes_key_c2s, key_c2s, sizeof(km->aes_key_c2s));
244 memcpy(km->aes_key_s2c, key_s2c, sizeof(km->aes_key_s2c));
245 memcpy(km->aes_iv_c2s, iv_c2s, sizeof(km->aes_iv_c2s));
246 memcpy(km->aes_iv_s2c, iv_s2c, sizeof(km->aes_iv_s2c));
247
248 // Wipe stack temporaries (key material).
249 pc_secure_wipe(key_c2s, sizeof(key_c2s));
250 pc_secure_wipe(key_s2c, sizeof(key_s2c));
251 pc_secure_wipe(iv_c2s, sizeof(iv_c2s));
252 pc_secure_wipe(iv_s2c, sizeof(iv_s2c));
253
254 km->active = true;
255}
256
257void ssh_dh_derive_keys(uint8_t i, const uint8_t K_be[256], const uint8_t H[PC_SHA256_DIGEST_LEN])
258{
259 // First-KEX convenience: session id equals H; aes256-ctr + hmac-sha2-256 (pre-negotiation defaults),
260 // SHA-256 exchange hash (h_len / sid_len / is512 default).
262}
const pc_bignum group14_g
Generator for group-14: g = 2.
Definition bignum.cpp:54
#define PC_BN_LIMBS
Number of 32-bit limbs in a 2048-bit integer.
Definition bignum.h:83
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
#define PC_CHACHAPOLY_KEY_LEN
two 256-bit ChaCha20 keys
Definition chachapoly.h:30
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
pc_aesgcm_key * pc_aesgcm_key_init(void *storage, const uint8_t key[PC_AESGCM_KEY_LEN])
Bind storage as a context keyed with key.
void pc_aesgcm_key_wipe(pc_aesgcm_key *k)
Wipe the expanded schedule. Call on rekey and on close; the storage stays the caller's.
void bn_expmod_group14(pc_bignum *out, const pc_bignum *base, const pc_bignum *exp)
Secure pool accessor - borrows that hold key material.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
void ssh_kdf_derive(const uint8_t K_be[256], const uint8_t *H, const uint8_t *session_id, char label, uint8_t *out, size_t out_len, bool k_is_string, size_t h_len, size_t sid_len, bool is512)
RFC 4253 §7.2 key derivation for any length up to SSH_KDF_MAX.
Definition ssh_dh.cpp:120
void ssh_rng_fill(uint8_t *buf, size_t len)
Fill len bytes of buf with cryptographically random data.
Definition ssh_dh.cpp:20
void ssh_dh_derive_keys(uint8_t i, const uint8_t K_be[256], const uint8_t H[PC_SHA256_DIGEST_LEN])
Derive the six session keys from shared secret K and exchange hash H.
Definition ssh_dh.cpp:257
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).
Definition ssh_dh.cpp:167
int ssh_dh_generate(uint8_t i)
Generate the server ephemeral DH key pair for connection slot i.
Definition ssh_dh.cpp:29
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:130
One key-exchange digest that dispatches SHA-256 or SHA-512 by the negotiated method.
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:121
@ SSH_CIPHER_AES256GCM
aes256-gcm@openssh.com (AEAD, RFC 5647; no separate MAC)
Definition ssh_keymat.h:122
@ SSH_CIPHER_AES256CTR
aes256-ctr + a separate HMAC (the fallback)
Definition ssh_keymat.h:120
@ SSH_MAC_HMAC_SHA256
hmac-sha2-256 (encrypt-and-MAC, RFC 4253)
Definition ssh_keymat.h:128
Ephemeral Diffie-Hellman state for one SSH connection.
Definition ssh_keymat.h:238
pc_bignum f
Server DH public value = g^y mod p (sent to client).
Definition ssh_keymat.h:240
bool kex_done
True once NEWKEYS has been sent and received.
Definition ssh_keymat.h:244
pc_bignum y
Server ephemeral private DH scalar (SENSITIVE - wiped after KEX).
Definition ssh_keymat.h:239
A key-exchange digest bound to one of the SSH KEX hashes (SHA-256 or SHA-512).
Definition ssh_kexhash.h:29
AES-256-CTR + HMAC-SHA2-256 session keys for one SSH connection.
Definition ssh_keymat.h:173
uint8_t aes_iv_c2s[PC_AES256CTR_CTR_LEN]
AES IV C→S (CTR counter / GCM nonce); advances per packet.
Definition ssh_keymat.h:182
uint8_t chacha_key_s2c[PC_CHACHAPOLY_KEY_LEN]
server-to-client, used only in chacha mode.
Definition ssh_keymat.h:192
uint8_t aes_iv_s2c[PC_AES256CTR_CTR_LEN]
AES IV S→C (CTR counter / GCM nonce); advances per packet.
Definition ssh_keymat.h:183
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:185
uint8_t gcm_ctx_s2c[PC_WORK_AESGCM]
keyed GCM context S→C (server seals outbound).
Definition ssh_keymat.h:203
uint8_t aes_key_c2s[PC_AES256CTR_KEY_LEN]
AES key C→S (server decrypts inbound).
Definition ssh_keymat.h:180
uint8_t cipher_mode
SSH_CIPHER_* selected for this session (0 = aes256-ctr).
Definition ssh_keymat.h:189
uint8_t mac_key_s2c[64]
HMAC key, server-to-client (aes mode).
Definition ssh_keymat.h:186
bool active
True once keys are installed after successful KEX.
Definition ssh_keymat.h:205
uint8_t chacha_key_c2s[PC_CHACHAPOLY_KEY_LEN]
client-to-server, used only in chacha mode.
Definition ssh_keymat.h:191
uint8_t mac_mode
SSH_MAC_* selected for the aes256-ctr cipher (0 = hmac-sha2-256 E&M).
Definition ssh_keymat.h:187
uint8_t aes_key_s2c[PC_AES256CTR_KEY_LEN]
AES key S→C (server encrypts outbound).
Definition ssh_keymat.h:181
uint8_t gcm_ctx_c2s[PC_WORK_AESGCM]
keyed GCM context C→S (server opens inbound).
Definition ssh_keymat.h:202
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition bignum.h:92
uint32_t d[PC_BN_LIMBS]
256 bytes of magnitude, little-endian limbs.
Definition bignum.h:93