DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_auth.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_auth.cpp
6 * @brief SSH user-authentication layer (RFC 4252) - password method.
7 */
8
10#include "network_drivers/presentation/ssh/crypto/ssh_ecdsa.h" // ssh_ecdsa_p256_verify() (ecdsa-sha2-nistp256)
11#include "network_drivers/presentation/ssh/crypto/ssh_ed25519.h" // ssh_ed25519_verify() (ssh-ed25519 client keys)
12#include "network_drivers/presentation/ssh/crypto/ssh_rsa.h" // ssh_rsa_verify(), SSH_RSA_KEY_BYTES
13#include "network_drivers/presentation/ssh/transport/ssh_packet.h" // SSH_MSG_* constants
15#include <string.h>
16
17// ---------------------------------------------------------------------------
18// Application password callback
19// ---------------------------------------------------------------------------
20
21// All SSH auth callbacks, owned by one instance (internal linkage): the application password
22// and public-key verifiers. One named owner, unreachable from any other translation unit.
24{
26 SshPubkeyCb pk_cb = nullptr;
27};
28static SshAuthCtx s_auth;
29
31{
32 s_auth.pw_cb = cb;
33}
34
36{
37 s_auth.pk_cb = cb;
38}
39
40// ---------------------------------------------------------------------------
41// Wire helpers
42// ---------------------------------------------------------------------------
43
44// Read an SSH string into a fixed buffer, null-terminating it. Advances *off.
45// Returns false on truncation or if the string does not fit (buffer too small).
46static bool read_string(const uint8_t *p, size_t len, size_t *off, char *out, size_t outcap)
47{
48 if (*off + 4 > len)
49 return false;
50 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
51 (uint32_t)p[*off + 3];
52 *off += 4;
53 if (*off + n > len)
54 return false;
55 if (n >= outcap)
56 return false; // does not fit our fixed buffer
57 memcpy(out, p + *off, n);
58 out[n] = '\0';
59 *off += n;
60 return true;
61}
62
63static void put_u32(uint8_t *p, uint32_t v)
64{
65 p[0] = (uint8_t)(v >> 24);
66 p[1] = (uint8_t)(v >> 16);
67 p[2] = (uint8_t)(v >> 8);
68 p[3] = (uint8_t)v;
69}
70
71// Read an SSH string by reference (no copy): *out points into p. Advances *off.
72static bool read_string_ref(const uint8_t *p, size_t len, size_t *off, const uint8_t **out, uint32_t *slen)
73{
74 if (*off + 4 > len)
75 return false;
76 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
77 (uint32_t)p[*off + 3];
78 *off += 4;
79 if (*off + n > len)
80 return false;
81 *out = p + *off;
82 *slen = n;
83 *off += n;
84 return true;
85}
86
87// Normalize an mpint (from a blob) into a fixed right-aligned big-endian buffer.
88static bool mpint_to_fixed(const uint8_t *m, uint32_t mlen, uint8_t *out, size_t outlen)
89{
90 uint32_t off = 0;
91 while (off < mlen && m[off] == 0) // strip sign/leading-zero bytes
92 off++;
93 uint32_t vlen = mlen - off;
94 if (vlen > outlen)
95 return false;
96 memset(out, 0, outlen);
97 memcpy(out + (outlen - vlen), m + off, vlen);
98 return true;
99}
100
101// Parse an "ssh-rsa" public-key blob: string("ssh-rsa") mpint(e) mpint(n).
102static bool parse_ssh_rsa_blob(const uint8_t *blob, uint32_t blen, uint8_t n_be[SSH_RSA_KEY_BYTES], uint8_t e_be[4])
103{
104 size_t off = 0;
105 const uint8_t *type;
106 uint32_t type_len;
107 if (!read_string_ref(blob, blen, &off, &type, &type_len))
108 return false;
109 if (type_len != 7 || memcmp(type, "ssh-rsa", 7) != 0)
110 return false;
111
112 const uint8_t *e_mp;
113 uint32_t e_len;
114 if (!read_string_ref(blob, blen, &off, &e_mp, &e_len))
115 return false;
116 if (!mpint_to_fixed(e_mp, e_len, e_be, 4))
117 return false;
118
119 const uint8_t *n_mp;
120 uint32_t n_len;
121 if (!read_string_ref(blob, blen, &off, &n_mp, &n_len))
122 return false;
123 if (!mpint_to_fixed(n_mp, n_len, n_be, SSH_RSA_KEY_BYTES))
124 return false;
125
126 return true;
127}
128
129// Parse an "ssh-ed25519" public-key blob: string("ssh-ed25519") string(pub32). (RFC 8709 §4)
130static bool parse_ssh_ed25519_blob(const uint8_t *blob, uint32_t blen, uint8_t pub[32])
131{
132 size_t off = 0;
133 const uint8_t *type;
134 uint32_t type_len;
135 if (!read_string_ref(blob, blen, &off, &type, &type_len))
136 return false;
137 if (type_len != 11 || memcmp(type, "ssh-ed25519", 11) != 0)
138 return false;
139 const uint8_t *pk;
140 uint32_t pk_len;
141 if (!read_string_ref(blob, blen, &off, &pk, &pk_len))
142 return false;
143 if (pk_len != 32)
144 return false;
145 memcpy(pub, pk, 32);
146 return true;
147}
148
149// Parse an "ecdsa-sha2-nistp256" public-key blob (RFC 5656 §3.1):
150// string("ecdsa-sha2-nistp256") string("nistp256") string(Q = 0x04||X||Y, 65 bytes).
151static bool parse_ssh_ecdsa_blob(const uint8_t *blob, uint32_t blen, uint8_t pub[SSH_ECDSA_P256_PUB_LEN])
152{
153 size_t off = 0;
154 const uint8_t *type;
155 uint32_t type_len;
156 if (!read_string_ref(blob, blen, &off, &type, &type_len))
157 return false;
158 if (type_len != 19 || memcmp(type, "ecdsa-sha2-nistp256", 19) != 0)
159 return false;
160 const uint8_t *curve;
161 uint32_t curve_len;
162 if (!read_string_ref(blob, blen, &off, &curve, &curve_len))
163 return false;
164 if (curve_len != 8 || memcmp(curve, "nistp256", 8) != 0)
165 return false;
166 const uint8_t *q;
167 uint32_t q_len;
168 if (!read_string_ref(blob, blen, &off, &q, &q_len))
169 return false;
170 if (q_len != SSH_ECDSA_P256_PUB_LEN || q[0] != 0x04) // uncompressed point only
171 return false;
172 memcpy(pub, q, SSH_ECDSA_P256_PUB_LEN);
173 return true;
174}
175
176// Parse an ECDSA signature blob (RFC 5656 §3.1.2): mpint(r) || mpint(s) -> raw r || s (32 + 32).
177static bool parse_ecdsa_sig(const uint8_t *sig, uint32_t slen, uint8_t out[SSH_ECDSA_P256_SIG_LEN])
178{
179 size_t off = 0;
180 const uint8_t *r;
181 const uint8_t *s;
182 uint32_t r_len;
183 uint32_t s_len;
184 if (!read_string_ref(sig, slen, &off, &r, &r_len) || !read_string_ref(sig, slen, &off, &s, &s_len))
185 return false;
186 return mpint_to_fixed(r, r_len, out, SSH_ECDSA_P256_COORD_LEN) &&
187 mpint_to_fixed(s, s_len, out + SSH_ECDSA_P256_COORD_LEN, SSH_ECDSA_P256_COORD_LEN);
188}
189
190// ---------------------------------------------------------------------------
191// Service request (RFC 4253 §10)
192// ---------------------------------------------------------------------------
193
194int ssh_auth_handle_service_request(const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
195{
196 if (len < 1 || payload[0] != SSH_MSG_SERVICE_REQUEST)
197 return -1;
198
199 size_t off = 1;
200 char svc[32];
201 if (!read_string(payload, len, &off, svc, sizeof(svc)))
202 return -1;
203 if (strcmp(svc, "ssh-userauth") != 0)
204 return -1;
205
206 // SERVICE_ACCEPT: byte(6) || string("ssh-userauth")
207 static const char name[] = "ssh-userauth";
208 uint32_t nl = (uint32_t)(sizeof(name) - 1);
209 if (cap < 1 + 4 + nl)
210 return -1;
211 out[0] = SSH_MSG_SERVICE_ACCEPT;
212 put_u32(out + 1, nl);
213 memcpy(out + 5, name, nl);
214 *out_len = 5 + nl;
215 return 0;
216}
217
218// ---------------------------------------------------------------------------
219// USERAUTH_REQUEST parse (RFC 4252 §5)
220// ---------------------------------------------------------------------------
221
222int ssh_auth_parse_request(const uint8_t *payload, size_t len, SshAuthReq *req)
223{
224 memset(req, 0, sizeof(*req));
225 if (len < 1 || payload[0] != SSH_MSG_USERAUTH_REQUEST)
226 return -1;
227
228 size_t off = 1;
229 if (!read_string(payload, len, &off, req->user, sizeof(req->user)))
230 return -1;
231 if (!read_string(payload, len, &off, req->service, sizeof(req->service)))
232 return -1;
233 if (!read_string(payload, len, &off, req->method, sizeof(req->method)))
234 return -1;
235
236 if (strcmp(req->method, "password") == 0)
237 {
238 // boolean (FALSE = not a password change) || string password
239 if (off >= len)
240 return -1;
241 off++; // skip the change-password boolean
242 if (!read_string(payload, len, &off, req->password, sizeof(req->password)))
243 return -1;
244 req->is_password = true;
245 }
246 else if (strcmp(req->method, "publickey") == 0)
247 {
248 // boolean has_signature || string algo || string pubkey-blob [|| string signature]
249 if (off >= len)
250 return -1;
251 req->has_signature = payload[off++] != 0;
252 if (!read_string(payload, len, &off, req->pk_algo, sizeof(req->pk_algo)))
253 return -1;
254 if (!read_string_ref(payload, len, &off, &req->pk_blob, &req->pk_blob_len))
255 return -1;
256
257 // Everything parsed so far is exactly the data the signature covers.
258 req->signed_prefix = payload;
259 req->signed_prefix_len = off;
260
261 if (req->has_signature)
262 {
263 const uint8_t *sigblob;
264 uint32_t sigblob_len;
265 if (!read_string_ref(payload, len, &off, &sigblob, &sigblob_len))
266 return -1;
267 // signature blob = string(sig-algo) || string(raw-signature)
268 size_t so = 0;
269 const uint8_t *salgo;
270 uint32_t salgo_len;
271 if (!read_string_ref(sigblob, sigblob_len, &so, &salgo, &salgo_len))
272 return -1;
273 if (!read_string_ref(sigblob, sigblob_len, &so, &req->signature, &req->signature_len))
274 return -1;
275 }
276 req->is_pubkey = true;
277 }
278 return 0;
279}
280
281// ---------------------------------------------------------------------------
282// Response builders
283// ---------------------------------------------------------------------------
284
285int ssh_auth_build_failure(uint8_t *out, size_t *out_len, size_t cap, bool partial)
286{
287 // SSH_MSG_USERAUTH_FAILURE || name-list(authentications) || boolean(partial)
288#if DETWS_SSH_ALLOW_PASSWORD
289 static const char methods[] = "publickey,password";
290#else
291 static const char methods[] = "publickey"; // password auth disabled for hardening
292#endif
293 uint32_t ml = (uint32_t)(sizeof(methods) - 1);
294 if (cap < 1 + 4 + ml + 1)
295 return -1;
297 put_u32(out + 1, ml);
298 memcpy(out + 5, methods, ml);
299 out[5 + ml] = partial ? 1 : 0;
300 *out_len = 5 + ml + 1;
301 return 0;
302}
303
304int ssh_auth_build_success(uint8_t *out, size_t *out_len, size_t cap)
305{
306 if (cap < 1)
307 return -1;
309 *out_len = 1;
310 return 0;
311}
312
313// SSH_MSG_USERAUTH_PK_OK || string(algo) || string(blob) - the "this key would
314// be accepted, send a signature" probe response (RFC 4252 §7).
315static int build_pk_ok(const SshAuthReq *req, uint8_t *out, size_t *out_len, size_t cap)
316{
317 uint32_t al = (uint32_t)strnlen(req->pk_algo, sizeof(req->pk_algo));
318 if (cap < (size_t)1 + 4 + al + 4 + req->pk_blob_len)
319 return -1;
320 size_t o = 0;
321 out[o++] = SSH_MSG_USERAUTH_PK_OK;
322 put_u32(out + o, al);
323 o += 4;
324 memcpy(out + o, req->pk_algo, al);
325 o += al;
326 put_u32(out + o, req->pk_blob_len);
327 o += 4;
328 memcpy(out + o, req->pk_blob, req->pk_blob_len);
329 o += req->pk_blob_len;
330 *out_len = o;
331 return 0;
332}
333
334// ---------------------------------------------------------------------------
335// Orchestration
336// ---------------------------------------------------------------------------
337
338// publickey method (RFC 4252 §7): validate the offered key (a signature-less probe -> PK_OK) or verify
339// the signature over string(session_id) || signed_prefix, keying success to connection i.
340static int ssh_auth_handle_pubkey(uint8_t i, const SshAuthReq *req, uint8_t *out, size_t *out_len, size_t cap)
341{
342 // Key type is taken from the blob (the algo name only steers the RSA signature hash).
343 bool is_ed = req->pk_blob_len >= 4 + 11 && memcmp(req->pk_blob,
344 "\x00\x00\x00\x0b"
345 "ssh-ed25519",
346 4 + 11) == 0;
347 bool is_ecdsa = req->pk_blob_len >= 4 + 19 && memcmp(req->pk_blob,
348 "\x00\x00\x00\x13"
349 "ecdsa-sha2-nistp256",
350 4 + 19) == 0;
351 uint8_t n_be[SSH_RSA_KEY_BYTES];
352 uint8_t e_be[4];
353 uint8_t ed_pub[32];
354 uint8_t ec_pub[SSH_ECDSA_P256_PUB_LEN];
355 bool parsed = false;
356 if (is_ed)
357 parsed = parse_ssh_ed25519_blob(req->pk_blob, req->pk_blob_len, ed_pub);
358 else if (is_ecdsa)
359 parsed = parse_ssh_ecdsa_blob(req->pk_blob, req->pk_blob_len, ec_pub);
360 else
361 parsed = parse_ssh_rsa_blob(req->pk_blob, req->pk_blob_len, n_be, e_be);
362 bool key_ok = parsed && s_auth.pk_cb && s_auth.pk_cb(req->user, req->pk_blob, req->pk_blob_len);
363 if (!key_ok)
364 return ssh_auth_build_failure(out, out_len, cap, false);
365
366 if (!req->has_signature)
367 return build_pk_ok(req, out, out_len, cap); // probe: ask for a signature
368
369 // Verify the signature over string(session_id) || signed_prefix.
370 uint8_t signed_data[SSH_PKT_BUF_SIZE + 4 + SSH_SHA256_DIGEST_LEN];
371 size_t sd = 0;
372 put_u32(signed_data + sd, SSH_SHA256_DIGEST_LEN);
373 sd += 4;
374 memcpy(signed_data + sd, ssh_sess[i].session_id, SSH_SHA256_DIGEST_LEN);
377 return ssh_auth_build_failure(out, out_len, cap, false);
378 memcpy(signed_data + sd, req->signed_prefix, req->signed_prefix_len);
379 sd += req->signed_prefix_len;
380
381 // For RSA the signature hash is chosen by the client's algorithm name (RFC 8332),
382 // not the key blob: rsa-sha2-512 -> SHA-512, otherwise SHA-256.
383 const SshRsaHash rh = (strcmp(req->pk_algo, SSH_RSA_SIG_ALG_SHA512) == 0) ? SshRsaHash::SHA512 : SshRsaHash::SHA256;
384 bool sig_ok;
385 if (is_ed)
386 {
387 sig_ok = req->signature_len == 64 && ssh_ed25519_verify(ed_pub, signed_data, sd, req->signature);
388 }
389 else if (is_ecdsa)
390 {
391 uint8_t ec_sig[SSH_ECDSA_P256_SIG_LEN];
392 sig_ok = parse_ecdsa_sig(req->signature, req->signature_len, ec_sig) &&
393 ssh_ecdsa_p256_verify(ec_pub, signed_data, sd, ec_sig);
394 }
395 else
396 {
397 sig_ok = ssh_rsa_verify(n_be, e_be, signed_data, sd, req->signature, req->signature_len, rh) == 0;
398 }
399 if (sig_ok)
400 {
401 ssh_sess[i].authed = true;
403 return ssh_auth_build_success(out, out_len, cap);
404 }
405 return ssh_auth_build_failure(out, out_len, cap, false);
406}
407
408int ssh_auth_handle_request(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
409{
410 if (i >= MAX_SSH_CONNS)
411 return -1;
412
413 SshAuthReq req;
414 if (ssh_auth_parse_request(payload, len, &req) != 0)
415 return -1;
416
417 // ---- publickey method (RFC 4252 §7) ----
418 if (req.is_pubkey)
419 return ssh_auth_handle_pubkey(i, &req, out, out_len, cap);
420
421 // ---- password method (RFC 4252 §8) ----
422 // Password auth can be compiled out for publickey-only hardening.
423#if DETWS_SSH_ALLOW_PASSWORD
424 bool ok = req.is_password && s_auth.pw_cb && s_auth.pw_cb(req.user, req.password);
425#else
426 bool ok = false;
427#endif
428
429 // Wipe the password from the stack regardless of the outcome.
430 volatile char *p = req.password;
431 for (size_t k = 0; k < sizeof(req.password); k++)
432 p[k] = 0;
433
434 if (ok)
435 {
436 ssh_sess[i].authed = true;
438 return ssh_auth_build_success(out, out_len, cap);
439 }
440 return ssh_auth_build_failure(out, out_len, cap, false);
441}
#define MAX_SSH_CONNS
Maximum simultaneous SSH connections.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
void ssh_auth_set_pubkey_cb(SshPubkeyCb cb)
Install the publickey-authorization callback (nullptr → all fail).
Definition ssh_auth.cpp:35
int ssh_auth_build_success(uint8_t *out, size_t *out_len, size_t cap)
Build SSH_MSG_USERAUTH_SUCCESS.
Definition ssh_auth.cpp:304
void ssh_auth_set_password_cb(SshPasswordCb cb)
Install the password-verification callback (nullptr → all fail).
Definition ssh_auth.cpp:30
int ssh_auth_handle_request(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle a USERAUTH_REQUEST end-to-end for slot i.
Definition ssh_auth.cpp:408
int ssh_auth_build_failure(uint8_t *out, size_t *out_len, size_t cap, bool partial)
Build SSH_MSG_USERAUTH_FAILURE advertising "password".
Definition ssh_auth.cpp:285
int ssh_auth_parse_request(const uint8_t *payload, size_t len, SshAuthReq *req)
Parse an SSH_MSG_USERAUTH_REQUEST into req.
Definition ssh_auth.cpp:222
int ssh_auth_handle_service_request(const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
Handle SSH_MSG_SERVICE_REQUEST; emit SERVICE_ACCEPT for ssh-userauth.
Definition ssh_auth.cpp:194
SSH user-authentication layer (RFC 4252).
bool(* SshPasswordCb)(const char *user, const char *password)
Application callback that validates a username/password pair.
Definition ssh_auth.h:54
bool(* SshPubkeyCb)(const char *user, const uint8_t *blob, size_t blob_len)
Application callback that decides whether a public key is authorized for user. blob is the "ssh-rsa" ...
Definition ssh_auth.h:64
bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[SSH_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
bool ssh_ed25519_verify(const uint8_t pub[32], const uint8_t *msg, size_t mlen, const uint8_t sig[64])
Ed25519 signatures (RFC 8032) for ssh-ed25519 host keys + client auth.
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_MSG_USERAUTH_REQUEST
Definition ssh_packet.h:106
#define SSH_MSG_USERAUTH_PK_OK
Definition ssh_packet.h:109
#define SSH_MSG_SERVICE_ACCEPT
Definition ssh_packet.h:100
#define SSH_MSG_SERVICE_REQUEST
Definition ssh_packet.h:99
#define SSH_MSG_USERAUTH_SUCCESS
Definition ssh_packet.h:108
#define SSH_MSG_USERAUTH_FAILURE
Definition ssh_packet.h:107
int ssh_rsa_verify(const uint8_t n_be[SSH_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len, const uint8_t *sig, size_t sig_len, SshRsaHash hash)
Verify an RSA PKCS#1 v1.5 signature (rsa-sha2-256/512) with a public key.
Definition ssh_rsa.cpp:181
RSA-SHA2-256/512 host-key signing and public-key serialization.
SshRsaHash
Hash algorithm selecting the RSA signature scheme (RFC 8332).
Definition ssh_rsa.h:106
@ SHA256
rsa-sha2-256
@ SHA512
rsa-sha2-512
#define SSH_RSA_KEY_BYTES
RSA modulus and private exponent size in bytes (RSA-2048).
Definition ssh_rsa.h:119
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29
SshSession ssh_sess[MAX_SSH_CONNS]
Static pool of SSH session state (BSS), one per SSH slot.
SSH transport-layer protocol state machine (RFC 4253).
@ SSH_PHASE_OPEN
Authenticated; connection/channel protocol active.
SshPasswordCb pw_cb
Definition ssh_auth.cpp:25
SshPubkeyCb pk_cb
Definition ssh_auth.cpp:26
Parsed SSH_MSG_USERAUTH_REQUEST.
Definition ssh_auth.h:31
const uint8_t * signature
Raw signature bytes (points into the payload).
Definition ssh_auth.h:44
bool is_pubkey
True if a publickey method-request was parsed.
Definition ssh_auth.h:39
char pk_algo[20]
Public-key algorithm name.
Definition ssh_auth.h:41
bool is_password
True if a password method-request was parsed.
Definition ssh_auth.h:36
char method[16]
Method name ("none", "password", "publickey").
Definition ssh_auth.h:34
char password[SSH_AUTH_PASS_MAX]
Password (method == "password").
Definition ssh_auth.h:35
char user[SSH_AUTH_USER_MAX]
User name, null-terminated.
Definition ssh_auth.h:32
uint32_t pk_blob_len
Length of pk_blob.
Definition ssh_auth.h:43
char service[32]
Requested service ("ssh-connection").
Definition ssh_auth.h:33
size_t signed_prefix_len
Length of signed_prefix (payload up to the signature).
Definition ssh_auth.h:47
const uint8_t * pk_blob
Public-key blob (points into the payload).
Definition ssh_auth.h:42
const uint8_t * signed_prefix
Bytes of the request that the signature covers.
Definition ssh_auth.h:46
uint32_t signature_len
Length of signature.
Definition ssh_auth.h:45
bool has_signature
True if the request carried a signature.
Definition ssh_auth.h:40
SshPhase phase
Current handshake phase.
bool authed
True after successful user authentication.