ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 "crypto/asymmetric/ecdsa.h" // pc_ecdsa_p256_verify() (ecdsa-sha2-nistp256)
11#include "crypto/asymmetric/ed25519.h" // pc_ed25519_verify() (ssh-ed25519 client keys)
12#include "crypto/crypto_scratch.h" // pc_secure_wipe() (the canonical secure wipe)
13#include "network_drivers/presentation/ssh/crypto/ssh_rsa.h" // pc_rsa_verify(), PC_RSA_KEY_BYTES
14#include "network_drivers/presentation/ssh/transport/ssh_packet.h" // SSH_MSG_* constants
16#include "server/mmgr/scratch.h" // pc_scratch_span() for the verify buffers
17#include "server/mmgr/secure.h"
18#include <string.h>
19
20// ---------------------------------------------------------------------------
21// Application password callback
22// ---------------------------------------------------------------------------
23
24// All SSH auth callbacks, owned by one instance (internal linkage): the application password
25// and public-key verifiers. One named owner, unreachable from any other translation unit.
27{
29 SshPubkeyCb pk_cb = nullptr;
30#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
31 // Per-slot keyboard-interactive exchange state: armed by a "keyboard-interactive" USERAUTH_REQUEST
32 // (we send one INFO_REQUEST), consumed by the matching INFO_RESPONSE. The user is remembered across
33 // the round-trip since the INFO_RESPONSE does not carry it.
34 struct
35 {
36 bool pending;
37 char user[SSH_AUTH_USER_MAX];
38 } ki[MAX_SSH_CONNS];
39#endif
40};
41static SshAuthCtx s_auth;
42
44{
45 s_auth.pw_cb = cb;
46}
47
49{
50 s_auth.pk_cb = cb;
51}
52
53// ---------------------------------------------------------------------------
54// Wire helpers
55// ---------------------------------------------------------------------------
56
57// Read an SSH string into a fixed buffer, null-terminating it. Advances *off.
58// Returns false on truncation or if the string does not fit (buffer too small).
59static bool read_string(const uint8_t *p, size_t len, size_t *off, char *out, size_t outcap)
60{
61 if (*off + 4 > len)
62 {
63 return false;
64 }
65 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
66 (uint32_t)p[*off + 3];
67 *off += 4;
68 if (*off + n > len)
69 {
70 return false;
71 }
72 if (n >= outcap)
73 {
74 return false; // does not fit our fixed buffer
75 }
76 memcpy(out, p + *off, n);
77 out[n] = '\0';
78 *off += n;
79 return true;
80}
81
82static void put_u32(uint8_t *p, uint32_t v)
83{
84 p[0] = (uint8_t)(v >> 24);
85 p[1] = (uint8_t)(v >> 16);
86 p[2] = (uint8_t)(v >> 8);
87 p[3] = (uint8_t)v;
88}
89
90// Read an SSH string by reference (no copy): *out points into p. Advances *off.
91static bool read_string_ref(const uint8_t *p, size_t len, size_t *off, const uint8_t **out, uint32_t *slen)
92{
93 if (*off + 4 > len)
94 {
95 return false;
96 }
97 uint32_t n = ((uint32_t)p[*off] << 24) | ((uint32_t)p[*off + 1] << 16) | ((uint32_t)p[*off + 2] << 8) |
98 (uint32_t)p[*off + 3];
99 *off += 4;
100 if (*off + n > len)
101 {
102 return false;
103 }
104 *out = p + *off;
105 *slen = n;
106 *off += n;
107 return true;
108}
109
110// Normalize an mpint (from a blob) into a fixed right-aligned big-endian buffer.
111static bool mpint_to_fixed(const uint8_t *m, uint32_t mlen, uint8_t *out, size_t outlen)
112{
113 uint32_t off = 0;
114 while (off < mlen && m[off] == 0) // strip sign/leading-zero bytes
115 {
116 off++;
117 }
118 uint32_t vlen = mlen - off;
119 if (vlen > outlen)
120 {
121 return false;
122 }
123 memset(out, 0, outlen);
124 memcpy(out + (outlen - vlen), m + off, vlen);
125 return true;
126}
127
128// Parse an "ssh-rsa" public-key blob: string("ssh-rsa") mpint(e) mpint(n).
129static bool parse_ssh_rsa_blob(const uint8_t *blob, uint32_t blen, uint8_t n_be[PC_RSA_KEY_BYTES], uint8_t e_be[4])
130{
131 size_t off = 0;
132 const uint8_t *type;
133 uint32_t type_len;
134 if (!read_string_ref(blob, blen, &off, &type, &type_len))
135 {
136 return false;
137 }
138 if (type_len != 7 || memcmp(type, "ssh-rsa", 7) != 0)
139 {
140 return false;
141 }
142
143 const uint8_t *e_mp;
144 uint32_t e_len;
145 if (!read_string_ref(blob, blen, &off, &e_mp, &e_len))
146 {
147 return false;
148 }
149 if (!mpint_to_fixed(e_mp, e_len, e_be, 4))
150 {
151 return false;
152 }
153
154 const uint8_t *n_mp;
155 uint32_t n_len;
156 if (!read_string_ref(blob, blen, &off, &n_mp, &n_len))
157 {
158 return false;
159 }
160 if (!mpint_to_fixed(n_mp, n_len, n_be, PC_RSA_KEY_BYTES))
161 {
162 return false;
163 }
164
165 return true;
166}
167
168// Parse an "ssh-ed25519" public-key blob: string("ssh-ed25519") string(pub32). (RFC 8709 §4)
169static bool parse_pc_ed25519_blob(const uint8_t *blob, uint32_t blen, uint8_t pub[32])
170{
171 size_t off = 0;
172 const uint8_t *type;
173 uint32_t type_len;
174 // The caller only reaches here after matching the 15-byte string("ssh-ed25519") prefix on the blob,
175 // so the type field is already proven present and correct.
176 if (!read_string_ref(blob, blen, &off, &type, &type_len)) // GCOVR_EXCL_LINE prefix match implies blen >= 15
177 {
178 return false; // GCOVR_EXCL_LINE
179 }
180 if (type_len != 11 || memcmp(type, "ssh-ed25519", 11) != 0) // GCOVR_EXCL_LINE prefix match implies this type
181 {
182 return false; // GCOVR_EXCL_LINE
183 }
184 const uint8_t *pk;
185 uint32_t pk_len;
186 if (!read_string_ref(blob, blen, &off, &pk, &pk_len))
187 {
188 return false;
189 }
190 if (pk_len != 32)
191 {
192 return false;
193 }
194 memcpy(pub, pk, 32);
195 return true;
196}
197
198// Parse an "ecdsa-sha2-nistp256" public-key blob (RFC 5656 §3.1):
199// string("ecdsa-sha2-nistp256") string("nistp256") string(Q = 0x04||X||Y, 65 bytes).
200static bool parse_pc_ecdsa_blob(const uint8_t *blob, uint32_t blen, uint8_t pub[PC_ECDSA_P256_PUB_LEN])
201{
202 size_t off = 0;
203 const uint8_t *type;
204 uint32_t type_len;
205 // As above: the caller matched the 23-byte string("ecdsa-sha2-nistp256") prefix before calling in.
206 if (!read_string_ref(blob, blen, &off, &type, &type_len)) // GCOVR_EXCL_LINE prefix match implies blen >= 23
207 {
208 return false; // GCOVR_EXCL_LINE
209 }
210 if (type_len != 19 || memcmp(type, "ecdsa-sha2-nistp256", 19) != 0) // GCOVR_EXCL_LINE prefix implies this type
211 {
212 return false; // GCOVR_EXCL_LINE
213 }
214 const uint8_t *curve;
215 uint32_t curve_len;
216 if (!read_string_ref(blob, blen, &off, &curve, &curve_len))
217 {
218 return false;
219 }
220 if (curve_len != 8 || memcmp(curve, "nistp256", 8) != 0)
221 {
222 return false;
223 }
224 const uint8_t *q;
225 uint32_t q_len;
226 if (!read_string_ref(blob, blen, &off, &q, &q_len))
227 {
228 return false;
229 }
230 if (q_len != PC_ECDSA_P256_PUB_LEN || q[0] != 0x04) // uncompressed point only
231 {
232 return false;
233 }
234 memcpy(pub, q, PC_ECDSA_P256_PUB_LEN);
235 return true;
236}
237
238// Parse an ECDSA signature blob (RFC 5656 §3.1.2): mpint(r) || mpint(s) -> raw r || s (32 + 32).
239static bool parse_ecdsa_sig(const uint8_t *sig, uint32_t slen, uint8_t out[PC_ECDSA_P256_SIG_LEN])
240{
241 size_t off = 0;
242 const uint8_t *r;
243 const uint8_t *s;
244 uint32_t r_len;
245 uint32_t s_len;
246 if (!read_string_ref(sig, slen, &off, &r, &r_len) || !read_string_ref(sig, slen, &off, &s, &s_len))
247 {
248 return false;
249 }
250 return mpint_to_fixed(r, r_len, out, PC_ECDSA_P256_COORD_LEN) &&
251 mpint_to_fixed(s, s_len, out + PC_ECDSA_P256_COORD_LEN, PC_ECDSA_P256_COORD_LEN);
252}
253
254// ---------------------------------------------------------------------------
255// Service request (RFC 4253 §10)
256// ---------------------------------------------------------------------------
257
258int pc_ssh_auth_handle_service_request(const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
259{
260 if (len < 1 || payload[0] != SSH_MSG_SERVICE_REQUEST)
261 {
262 return -1;
263 }
264
265 size_t off = 1;
266 char svc[32];
267 if (!read_string(payload, len, &off, svc, sizeof(svc)))
268 {
269 return -1;
270 }
271 if (strcmp(svc, "ssh-userauth") != 0)
272 {
273 return -1;
274 }
275
276 // SERVICE_ACCEPT: byte(6) || string("ssh-userauth")
277 static const char name[] = "ssh-userauth";
278 uint32_t nl = (uint32_t)(sizeof(name) - 1);
279 if (cap < 1 + 4 + nl)
280 {
281 return -1;
282 }
283 out[0] = SSH_MSG_SERVICE_ACCEPT;
284 put_u32(out + 1, nl);
285 memcpy(out + 5, name, nl);
286 *out_len = 5 + nl;
287 return 0;
288}
289
290// ---------------------------------------------------------------------------
291// USERAUTH_REQUEST parse (RFC 4252 §5)
292// ---------------------------------------------------------------------------
293
294int pc_ssh_auth_parse_request(const uint8_t *payload, size_t len, SshAuthReq *req)
295{
296 memset(req, 0, sizeof(*req));
297 if (len < 1 || payload[0] != SSH_MSG_USERAUTH_REQUEST)
298 {
299 return -1;
300 }
301
302 size_t off = 1;
303 if (!read_string(payload, len, &off, req->user, sizeof(req->user)))
304 {
305 return -1;
306 }
307 if (!read_string(payload, len, &off, req->service, sizeof(req->service)))
308 {
309 return -1;
310 }
311 if (!read_string(payload, len, &off, req->method, sizeof(req->method)))
312 {
313 return -1;
314 }
315
316 if (strcmp(req->method, "password") == 0)
317 {
318 // boolean (FALSE = not a password change) || string password
319 if (off >= len)
320 {
321 return -1;
322 }
323 off++; // skip the change-password boolean
324 if (!read_string(payload, len, &off, req->password, sizeof(req->password)))
325 {
326 return -1;
327 }
328 req->is_password = true;
329 }
330 else if (strcmp(req->method, "publickey") == 0)
331 {
332 // boolean has_signature || string algo || string pubkey-blob [|| string signature]
333 if (off >= len)
334 {
335 return -1;
336 }
337 req->has_signature = payload[off++] != 0;
338 if (!read_string(payload, len, &off, req->pk_algo, sizeof(req->pk_algo)))
339 {
340 return -1;
341 }
342 if (!read_string_ref(payload, len, &off, &req->pk_blob, &req->pk_blob_len))
343 {
344 return -1;
345 }
346
347 // Everything parsed so far is exactly the data the signature covers.
348 req->signed_prefix = payload;
349 req->signed_prefix_len = off;
350
351 if (req->has_signature)
352 {
353 const uint8_t *sigblob;
354 uint32_t sigblob_len;
355 if (!read_string_ref(payload, len, &off, &sigblob, &sigblob_len))
356 {
357 return -1;
358 }
359 // signature blob = string(sig-algo) || string(raw-signature)
360 size_t so = 0;
361 const uint8_t *salgo;
362 uint32_t salgo_len;
363 if (!read_string_ref(sigblob, sigblob_len, &so, &salgo, &salgo_len))
364 {
365 return -1;
366 }
367 if (!read_string_ref(sigblob, sigblob_len, &so, &req->signature, &req->signature_len))
368 {
369 return -1;
370 }
371 }
372 req->is_pubkey = true;
373 }
374#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
375 else if (strcmp(req->method, "keyboard-interactive") == 0)
376 {
377 // RFC 4256 §3.1: string(language tag, deprecated) || string(submethods). Both are ignored -
378 // this server always drives a single "Password:" prompt.
379 req->is_kbdint = true;
380 }
381#endif
382 return 0;
383}
384
385// ---------------------------------------------------------------------------
386// Response builders
387// ---------------------------------------------------------------------------
388
389int pc_ssh_auth_build_failure(uint8_t *out, size_t *out_len, size_t cap, bool partial)
390{
391 // SSH_MSG_USERAUTH_FAILURE || name-list(authentications) || boolean(partial)
392#if PC_SSH_ALLOW_PASSWORD
393#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
394 static const char methods[] = "publickey,password,keyboard-interactive";
395#else
396 static const char methods[] = "publickey,password";
397#endif
398#else
399 static const char methods[] = "publickey"; // password auth disabled for hardening
400#endif
401 uint32_t ml = (uint32_t)(sizeof(methods) - 1);
402 if (cap < 1 + 4 + ml + 1)
403 {
404 return -1;
405 }
407 put_u32(out + 1, ml);
408 memcpy(out + 5, methods, ml);
409 out[5 + ml] = partial ? 1 : 0;
410 *out_len = 5 + ml + 1;
411 return 0;
412}
413
414int pc_ssh_auth_build_success(uint8_t *out, size_t *out_len, size_t cap)
415{
416 if (cap < 1)
417 {
418 return -1;
419 }
421 *out_len = 1;
422 return 0;
423}
424
425// SSH_MSG_USERAUTH_PK_OK || string(algo) || string(blob) - the "this key would
426// be accepted, send a signature" probe response (RFC 4252 §7).
427static int build_pk_ok(const SshAuthReq *req, uint8_t *out, size_t *out_len, size_t cap)
428{
429 uint32_t al = (uint32_t)strnlen(req->pk_algo, sizeof(req->pk_algo));
430 if (cap < (size_t)1 + 4 + al + 4 + req->pk_blob_len)
431 {
432 return -1;
433 }
434 size_t o = 0;
435 out[o++] = SSH_MSG_USERAUTH_PK_OK;
436 put_u32(out + o, al);
437 o += 4;
438 memcpy(out + o, req->pk_algo, al);
439 o += al;
440 put_u32(out + o, req->pk_blob_len);
441 o += 4;
442 memcpy(out + o, req->pk_blob, req->pk_blob_len);
443 o += req->pk_blob_len;
444 *out_len = o;
445 return 0;
446}
447
448#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
449// SSH_MSG_USERAUTH_INFO_REQUEST (RFC 4256 §3.2): empty name/instruction/language, one non-echoed
450// "Password: " prompt. This is the challenge-response face of password auth (a single prompt).
451static int build_info_request(uint8_t *out, size_t *out_len, size_t cap)
452{
453 static const char prompt[] = "Password: ";
454 const uint32_t pl = (uint32_t)(sizeof(prompt) - 1);
455 const size_t need = 1 + 4 + 4 + 4 + 4 + 4 + pl + 1; // msg,name,instr,lang,num-prompts,prompt,echo
456 if (cap < need)
457 {
458 return -1;
459 }
460 size_t o = 0;
462 put_u32(out + o, 0); // name = ""
463 o += 4;
464 put_u32(out + o, 0); // instruction = ""
465 o += 4;
466 put_u32(out + o, 0); // language tag = "" (deprecated)
467 o += 4;
468 put_u32(out + o, 1); // num-prompts = 1
469 o += 4;
470 put_u32(out + o, pl);
471 o += 4;
472 memcpy(out + o, prompt, pl);
473 o += pl;
474 out[o++] = 0; // echo = FALSE (the response is a password)
475 *out_len = o;
476 return 0;
477}
478#endif
479
480// ---------------------------------------------------------------------------
481// Orchestration
482// ---------------------------------------------------------------------------
483
484// publickey method (RFC 4252 §7): validate the offered key (a signature-less probe -> PK_OK) or verify
485// the signature over string(session_id) || signed_prefix, keying success to connection i.
486static int pc_ssh_auth_handle_pubkey(uint8_t i, const SshAuthReq *req, uint8_t *out, size_t *out_len, size_t cap)
487{
488 // Key type is taken from the blob (the algo name only steers the RSA signature hash).
489 bool is_ed = req->pk_blob_len >= 4 + 11 && memcmp(req->pk_blob,
490 "\x00\x00\x00\x0b"
491 "ssh-ed25519",
492 4 + 11) == 0;
493 bool is_ecdsa = req->pk_blob_len >= 4 + 19 && memcmp(req->pk_blob,
494 "\x00\x00\x00\x13"
495 "ecdsa-sha2-nistp256",
496 4 + 19) == 0;
497 // Borrowed for this dispatch rather than carried on the worker stack. This function sits on the
498 // deepest call chain in the library (dispatch -> auth -> ed25519 verify -> ed_add), so the key
499 // material and the signed-data staging buffer are what drive the worker stack requirement.
500 // pc_scratch_span binds each capacity to its allocation, so the bounds below are the reserved
501 // sizes rather than a second set of constants that has to be kept in step by hand.
503 const pc_span &n_be = n_be_b.span();
504 pc_span e_be = pc_scratch_span(4, 4);
505 pc_span ed_pub = pc_scratch_span(32, 4);
507 if (!pc_span_ok(n_be) || !pc_span_ok(e_be) || !pc_span_ok(ed_pub) || !pc_span_ok(ec_pub))
508 {
509 return pc_ssh_auth_build_failure(out, out_len, cap, false); // arena exhausted: fail closed
510 }
511 bool parsed = false;
512 if (is_ed)
513 {
514 parsed = parse_pc_ed25519_blob(req->pk_blob, req->pk_blob_len, ed_pub.buf);
515 }
516 else if (is_ecdsa)
517 {
518 parsed = parse_pc_ecdsa_blob(req->pk_blob, req->pk_blob_len, ec_pub.buf);
519 }
520 else
521 {
522 parsed = parse_ssh_rsa_blob(req->pk_blob, req->pk_blob_len, n_be.buf, e_be.buf);
523 }
524 bool key_ok = parsed && s_auth.pk_cb && s_auth.pk_cb(req->user, req->pk_blob, req->pk_blob_len);
525 if (!key_ok)
526 {
527 return pc_ssh_auth_build_failure(out, out_len, cap, false);
528 }
529
530 if (!req->has_signature)
531 {
532 return build_pk_ok(req, out, out_len, cap); // probe: ask for a signature
533 }
534
535 // Verify the signature over string(session_id) || signed_prefix. The session_id is the first KEX's
536 // exchange hash: 32 bytes (SHA-256 methods) or 64 (sntrup761x25519-sha512).
537 const size_t sid_len = ssh_sess[i].session_id_len;
539 if (!pc_span_ok(signed_data))
540 {
541 return pc_ssh_auth_build_failure(out, out_len, cap, false); // arena exhausted: fail closed
542 }
543 if (req->signed_prefix_len > SSH_PKT_BUF_SIZE || 4 + sid_len + req->signed_prefix_len > signed_data.cap)
544 {
545 return pc_ssh_auth_build_failure(out, out_len, cap, false);
546 }
547 size_t sd = 0;
548 put_u32(signed_data.buf + sd, (uint32_t)sid_len);
549 sd += 4;
550 memcpy(signed_data.buf + sd, ssh_sess[i].session_id, sid_len);
551 sd += sid_len;
552 memcpy(signed_data.buf + sd, req->signed_prefix, req->signed_prefix_len);
553 sd += req->signed_prefix_len;
554
555 // For RSA the signature hash is chosen by the client's algorithm name (RFC 8332),
556 // not the key blob: rsa-sha2-512 -> SHA-512, otherwise SHA-256.
557 const pc_rsa_hash rh =
558 (strcmp(req->pk_algo, SSH_RSA_SIG_ALG_SHA512) == 0) ? pc_rsa_hash::SHA512 : pc_rsa_hash::SHA256;
559 bool sig_ok;
560 if (is_ed)
561 {
562 sig_ok = req->signature_len == 64 && pc_ed25519_verify(ed_pub.buf, signed_data.buf, sd, req->signature);
563 }
564 else if (is_ecdsa)
565 {
567 sig_ok = pc_span_ok(ec_sig) && parse_ecdsa_sig(req->signature, req->signature_len, ec_sig.buf) &&
568 pc_ecdsa_p256_verify(ec_pub.buf, signed_data.buf, sd, ec_sig.buf);
569 }
570 else
571 {
572 sig_ok = pc_rsa_verify(n_be.buf, e_be.buf, signed_data.buf, sd, req->signature, req->signature_len, rh) == 0;
573 }
574 if (sig_ok)
575 {
576 ssh_sess[i].authed = true;
578 return pc_ssh_auth_build_success(out, out_len, cap);
579 }
580 return pc_ssh_auth_build_failure(out, out_len, cap, false);
581}
582
583int pc_ssh_auth_handle_request(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len, size_t cap)
584{
585 if (i >= MAX_SSH_CONNS)
586 {
587 return -1;
588 }
589
590 SshAuthReq req;
591 if (pc_ssh_auth_parse_request(payload, len, &req) != 0)
592 {
593 return -1;
594 }
595
596 // ---- publickey method (RFC 4252 §7) ----
597 if (req.is_pubkey)
598 {
599 return pc_ssh_auth_handle_pubkey(i, &req, out, out_len, cap);
600 }
601
602#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
603 // ---- keyboard-interactive method (RFC 4256): arm the exchange and send one "Password:" prompt.
604 if (req.is_kbdint)
605 {
606 if (!s_auth.pw_cb) // no verifier installed -> cannot challenge
607 {
608 return pc_ssh_auth_build_failure(out, out_len, cap, false);
609 }
610 s_auth.ki[i].pending = true;
611 size_t ul = strnlen(req.user, sizeof(s_auth.ki[i].user) - 1);
612 memcpy(s_auth.ki[i].user, req.user, ul);
613 s_auth.ki[i].user[ul] = '\0';
614 return build_info_request(out, out_len, cap);
615 }
616#endif
617
618 // ---- password method (RFC 4252 §8) ----
619 // Password auth can be compiled out for publickey-only hardening.
620#if PC_SSH_ALLOW_PASSWORD
621 bool ok = req.is_password && s_auth.pw_cb && s_auth.pw_cb(req.user, req.password);
622#else
623 bool ok = false;
624#endif
625
626 // Wipe the password from the stack regardless of the outcome.
627 pc_secure_wipe(req.password, sizeof(req.password));
628
629 if (ok)
630 {
631 ssh_sess[i].authed = true;
633 return pc_ssh_auth_build_success(out, out_len, cap);
634 }
635 return pc_ssh_auth_build_failure(out, out_len, cap, false);
636}
637
638#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
639int pc_ssh_auth_handle_info_response(uint8_t i, const uint8_t *payload, size_t len, uint8_t *out, size_t *out_len,
640 size_t cap)
641{
642 if (i >= MAX_SSH_CONNS)
643 {
644 return -1;
645 }
646 if (!s_auth.ki[i].pending) // no keyboard-interactive exchange armed for this slot
647 {
648 return -1;
649 }
650 s_auth.ki[i].pending = false; // consume the exchange regardless of outcome
651
652 // SSH_MSG_USERAUTH_INFO_RESPONSE (RFC 4256 §3.4): byte(61) || uint32 num-responses || string[num].
653 // We sent one prompt, so exactly one response is expected.
654 if (len < 1 || payload[0] != SSH_MSG_USERAUTH_INFO_RESPONSE)
655 {
656 return -1;
657 }
658 size_t off = 1;
659 if (off + 4 > len)
660 {
661 return -1;
662 }
663 uint32_t nr = ((uint32_t)payload[off] << 24) | ((uint32_t)payload[off + 1] << 16) |
664 ((uint32_t)payload[off + 2] << 8) | (uint32_t)payload[off + 3];
665 off += 4;
666
667 char resp[SSH_AUTH_PASS_MAX];
668 bool ok = false;
669 if (nr == 1 && read_string(payload, len, &off, resp, sizeof(resp)))
670 {
671 ok = s_auth.pw_cb && s_auth.pw_cb(s_auth.ki[i].user, resp);
672 }
673
674 // Wipe the response and the remembered user from memory regardless of outcome.
675 pc_secure_wipe(resp, sizeof(resp));
676 pc_secure_wipe(s_auth.ki[i].user, sizeof(s_auth.ki[i].user));
677
678 if (ok)
679 {
680 ssh_sess[i].authed = true;
682 return pc_ssh_auth_build_success(out, out_len, cap);
683 }
684 return pc_ssh_auth_build_failure(out, out_len, cap, false);
685}
686#endif
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
A scratch borrow whose acquire and release are one call each.
Definition scratch.h:168
Constant-time comparison for secret-dependent checks.
bool pc_ecdsa_p256_verify(const uint8_t pub[PC_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[PC_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
Definition ecdsa.cpp:159
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
#define PC_ECDSA_P256_COORD_LEN
P-256 coordinate length (one of X, Y).
Definition ecdsa.h:57
#define PC_ECDSA_P256_SIG_LEN
Raw ECDSA signature length: r || s (32 + 32, big-endian).
Definition ecdsa.h:61
#define PC_ECDSA_P256_PUB_LEN
P-256 uncompressed public point length: 0x04 || X || Y.
Definition ecdsa.h:59
bool pc_ed25519_verify(const uint8_t pub[32], const uint8_t *msg, size_t mlen, const uint8_t sig[64])
Definition ed25519.cpp:647
Ed25519 signatures (RFC 8032) for ssh-ed25519 host keys + client auth.
#define SSH_AUTH_PASS_MAX
Max stored password length.
#define SSH_AUTH_USER_MAX
Max stored user name (RFC 4252 imposes no limit; we cap for BSS).
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
int pc_rsa_verify(const uint8_t n_be[PC_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, pc_rsa_hash hash)
Verify an RSA-2048 PKCS#1 v1.5 signature over msg.
Definition rsa.cpp:55
pc_rsa_hash
Hash algorithm selecting the RSA signature scheme (RFC 8017 §9.2).
Definition rsa.h:39
@ SHA256
RSASSA-PKCS1-v1.5 with SHA-256.
@ SHA512
RSASSA-PKCS1-v1.5 with SHA-512.
#define PC_RSA_KEY_BYTES
RSA modulus / signature size in bytes (RSA-2048).
Definition rsa.h:28
pc_span pc_scratch_span(size_t n, size_t align)
Borrow n bytes as a span whose capacity is bound to the allocation.
Definition scratch.cpp:146
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
Secure pool accessor - borrows that hold key material.
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
void pc_ssh_auth_set_pubkey_cb(SshPubkeyCb cb)
Install the publickey-authorization callback (nullptr → all fail).
Definition ssh_auth.cpp:48
int pc_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:389
int pc_ssh_auth_build_success(uint8_t *out, size_t *out_len, size_t cap)
Build SSH_MSG_USERAUTH_SUCCESS.
Definition ssh_auth.cpp:414
void pc_ssh_auth_set_password_cb(SshPasswordCb cb)
Install the password-verification callback (nullptr → all fail).
Definition ssh_auth.cpp:43
int pc_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:294
int pc_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:258
int pc_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:583
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:55
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:65
#define SSH_KEXHASH_MAX_LEN
longest exchange-hash / session_id (SHA-512)
Definition ssh_kexhash.h:25
SSH binary packet protocol: framing, AES-256-CTR encryption, HMAC-SHA2-256 integrity,...
#define SSH_MSG_USERAUTH_INFO_REQUEST
Definition ssh_packet.h:112
#define SSH_MSG_USERAUTH_INFO_RESPONSE
Definition ssh_packet.h:113
#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
SSH RSA host-key layer: NVS-backed host key, host-key signing, and "ssh-rsa" blob encoding.
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:28
SshPubkeyCb pk_cb
Definition ssh_auth.cpp:29
Parsed SSH_MSG_USERAUTH_REQUEST.
Definition ssh_auth.h:31
char method[24]
Method name ("none", "password", "publickey", "keyboard-interactive").
Definition ssh_auth.h:34
const uint8_t * signature
Raw signature bytes (points into the payload).
Definition ssh_auth.h:45
bool is_pubkey
True if a publickey method-request was parsed.
Definition ssh_auth.h:40
char pk_algo[20]
Public-key algorithm name.
Definition ssh_auth.h:42
bool is_password
True if a password method-request was parsed.
Definition ssh_auth.h:36
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:44
char service[32]
Requested service ("ssh-connection").
Definition ssh_auth.h:33
bool is_kbdint
True if a keyboard-interactive method-request was parsed (RFC 4256).
Definition ssh_auth.h:37
size_t signed_prefix_len
Length of signed_prefix (payload up to the signature).
Definition ssh_auth.h:48
const uint8_t * pk_blob
Public-key blob (points into the payload).
Definition ssh_auth.h:43
const uint8_t * signed_prefix
Bytes of the request that the signature covers.
Definition ssh_auth.h:47
uint32_t signature_len
Length of signature.
Definition ssh_auth.h:46
bool has_signature
True if the request carried a signature.
Definition ssh_auth.h:41
uint8_t session_id_len
session_id length (the first KEX's exchange-hash length).
SshPhase phase
Current handshake phase.
bool authed
True after successful user authentication.
uint8_t session_id[SSH_KEXHASH_MAX_LEN]
H from the first KEX (RFC 4253 §7.2); 32 or 64 bytes.
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66
size_t cap
bytes writable at buf (0 whenever buf is nullptr)
Definition span.h:67