Coverage Report

Created: 2026-10-23 07:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssh/regress/misc/fuzz-harness/kex_fuzz.cc
Line
Count
Source
1
// libfuzzer driver for key exchange fuzzing.
2
3
4
#include <sys/types.h>
5
#include <stdio.h>
6
#include <stdint.h>
7
#include <stdlib.h>
8
#include <string.h>
9
10
extern "C" {
11
12
#include "includes.h"
13
#include "ssherr.h"
14
#include "ssh_api.h"
15
#include "sshbuf.h"
16
#include "packet.h"
17
#include "myproposal.h"
18
#include "xmalloc.h"
19
#include "authfile.h"
20
#include "log.h"
21
22
#include "fixed-keys.h"
23
24
// Define if you want to generate traces.
25
/* #define STANDALONE 1 */
26
27
static int prepare_key(struct shared_state *st, int keytype, int bits);
28
29
struct shared_state {
30
  size_t nkeys;
31
  struct sshkey **privkeys, **pubkeys;
32
};
33
34
struct test_state {
35
  struct sshbuf *smsgs, *cmsgs; /* output, for standalone mode */
36
  struct sshbuf *sin, *cin; /* input; setup per-test in do_kex_with_key */
37
  struct sshbuf *s_template, *c_template; /* main copy of input */
38
};
39
40
/*
41
 * Optional proposal overrides applied to both sides. Each field
42
 * replaces one KEXINIT slot when non-NULL.
43
 */
44
struct kex_overrides {
45
  const char *cipher;
46
  const char *mac;
47
  const char *comp;
48
};
49
50
static int
51
do_send_and_receive(struct ssh *from, struct ssh *to,
52
    struct sshbuf *store, int clobber, size_t *n)
53
289k
{
54
289k
  u_char type;
55
289k
  size_t len;
56
289k
  const u_char *buf;
57
289k
  int r;
58
59
544k
  for (*n = 0;; (*n)++) {
60
544k
    if ((r = ssh_packet_next(from, &type)) != 0) {
61
163k
      debug_fr(r, "ssh_packet_next");
62
163k
      return r;
63
163k
    }
64
381k
    if (type != 0)
65
16.1k
      return 0;
66
365k
    buf = ssh_output_ptr(from, &len);
67
365k
    debug_f("%zu%s", len, clobber ? " ignore" : "");
68
365k
    if (len == 0)
69
110k
      return 0;
70
255k
    if ((r = ssh_output_consume(from, len)) != 0) {
71
0
      debug_fr(r, "ssh_output_consume");
72
0
      return r;
73
0
    }
74
255k
    if (store != NULL && (r = sshbuf_put(store, buf, len)) != 0) {
75
0
      debug_fr(r, "sshbuf_put");
76
0
      return r;
77
0
    }
78
255k
    if (!clobber && (r = ssh_input_append(to, buf, len)) != 0) {
79
0
      debug_fr(r, "ssh_input_append");
80
0
      return r;
81
0
    }
82
255k
  }
83
289k
}
84
85
static int
86
run_kex(struct test_state *ts, struct ssh *client, struct ssh *server)
87
167k
{
88
167k
  int r = 0;
89
167k
  size_t cn, sn;
90
91
  /* If fuzzing, replace server/client input */
92
167k
  if (ts->sin != NULL) {
93
83.6k
    if ((r = ssh_input_append(server, sshbuf_ptr(ts->sin),
94
83.6k
        sshbuf_len(ts->sin))) != 0) {
95
0
      error_fr(r, "ssh_input_append");
96
0
      return r;
97
0
    }
98
83.6k
    sshbuf_reset(ts->sin);
99
83.6k
  }
100
167k
  if (ts->cin != NULL) {
101
83.6k
    if ((r = ssh_input_append(client, sshbuf_ptr(ts->cin),
102
83.6k
        sshbuf_len(ts->cin))) != 0) {
103
0
      error_fr(r, "ssh_input_append");
104
0
      return r;
105
0
    }
106
83.6k
    sshbuf_reset(ts->cin);
107
83.6k
  }
108
185k
  while (!server->kex->done || !client->kex->done) {
109
185k
    cn = sn = 0;
110
185k
    debug_f("S:");
111
185k
    if ((r = do_send_and_receive(server, client,
112
185k
        ts->smsgs, ts->cin != NULL, &sn)) != 0) {
113
81.2k
      debug_fr(r, "S->C");
114
81.2k
      break;
115
81.2k
    }
116
104k
    debug_f("C:");
117
104k
    if ((r = do_send_and_receive(client, server,
118
104k
        ts->cmsgs, ts->sin != NULL, &cn)) != 0) {
119
81.8k
      debug_fr(r, "C->S");
120
81.8k
      break;
121
81.8k
    }
122
22.2k
    if (cn == 0 && sn == 0) {
123
4.15k
      debug_f("kex stalled");
124
4.15k
      r = SSH_ERR_PROTOCOL_ERROR;
125
4.15k
      break;
126
4.15k
    }
127
22.2k
  }
128
167k
  debug_fr(r, "done");
129
167k
  return r;
130
167k
}
131
132
static void
133
store_key(struct shared_state *st, struct sshkey *pubkey,
134
    struct sshkey *privkey)
135
3
{
136
3
  if (st == NULL || pubkey->type < 0 || pubkey->type > INT_MAX ||
137
3
      privkey->type != pubkey->type ||
138
3
      ((size_t)pubkey->type < st->nkeys &&
139
0
       st->pubkeys[pubkey->type] != NULL))
140
0
    abort();
141
3
  if ((size_t)pubkey->type >= st->nkeys) {
142
3
    st->pubkeys = (struct sshkey **)xrecallocarray(st->pubkeys,
143
3
        st->nkeys, pubkey->type + 1, sizeof(*st->pubkeys));
144
3
    st->privkeys = (struct sshkey **)xrecallocarray(st->privkeys,
145
3
        st->nkeys, privkey->type + 1, sizeof(*st->privkeys));
146
3
    st->nkeys = privkey->type + 1;
147
3
  }
148
3
  debug_f("store %s at %d", sshkey_ssh_name(pubkey), pubkey->type);
149
3
  st->pubkeys[pubkey->type] = pubkey;
150
3
  st->privkeys[privkey->type] = privkey;
151
3
}
152
153
static int
154
prepare_keys(struct shared_state *st)
155
1
{
156
1
  if (prepare_key(st, KEY_RSA, 2048) != 0 ||
157
1
      prepare_key(st, KEY_ECDSA, 256) != 0 ||
158
1
      prepare_key(st, KEY_ED25519, 256) != 0) {
159
0
    error_f("key prepare failed");
160
0
    return -1;
161
0
  }
162
1
  return 0;
163
1
}
164
165
static struct sshkey *
166
get_pubkey(struct shared_state *st, int keytype)
167
167k
{
168
167k
  if (st == NULL || keytype < 0 || (size_t)keytype >= st->nkeys ||
169
167k
      st->pubkeys == NULL || st->pubkeys[keytype] == NULL)
170
0
    abort();
171
167k
  return st->pubkeys[keytype];
172
167k
}
173
174
static struct sshkey *
175
get_privkey(struct shared_state *st, int keytype)
176
167k
{
177
167k
  if (st == NULL || keytype < 0 || (size_t)keytype >= st->nkeys ||
178
167k
      st->privkeys == NULL || st->privkeys[keytype] == NULL)
179
0
    abort();
180
167k
  return st->privkeys[keytype];
181
167k
}
182
183
static int
184
build_proposal_side(char **out, const char *defaults[PROPOSAL_MAX],
185
    const char *kex, const char *keyname,
186
    const char *cipher, const char *mac, const char *comp)
187
167k
{
188
167k
  int i;
189
1.84M
  for (i = 0; i < PROPOSAL_MAX; i++) {
190
1.67M
    const char *ccp = defaults[i];
191
1.67M
#ifdef CIPHER_NONE_AVAIL
192
1.67M
    if (i == PROPOSAL_ENC_ALGS_CTOS ||
193
1.50M
        i == PROPOSAL_ENC_ALGS_STOC)
194
334k
      ccp = "none";
195
1.67M
#endif
196
1.67M
    if (i == PROPOSAL_SERVER_HOST_KEY_ALGS)
197
167k
      ccp = keyname;
198
1.50M
    else if (i == PROPOSAL_KEX_ALGS && kex != NULL)
199
167k
      ccp = kex;
200
1.33M
    else if ((i == PROPOSAL_ENC_ALGS_CTOS ||
201
1.17M
        i == PROPOSAL_ENC_ALGS_STOC) && cipher != NULL)
202
86.7k
      ccp = cipher;
203
1.25M
    else if ((i == PROPOSAL_MAC_ALGS_CTOS ||
204
1.08M
        i == PROPOSAL_MAC_ALGS_STOC) && mac != NULL)
205
37.1k
      ccp = mac;
206
1.21M
    else if ((i == PROPOSAL_COMP_ALGS_CTOS ||
207
1.04M
        i == PROPOSAL_COMP_ALGS_STOC) && comp != NULL)
208
24.7k
      ccp = comp;
209
1.67M
    if ((out[i] = strdup(ccp)) == NULL)
210
0
      return -1;
211
1.67M
  }
212
167k
  return 0;
213
167k
}
214
215
static int
216
do_kex_with_key(struct shared_state *st, struct test_state *ts,
217
    const char *kex, int keytype, const struct kex_overrides *ov)
218
167k
{
219
167k
  struct ssh *client = NULL, *server = NULL;
220
167k
  struct sshkey *privkey = NULL, *pubkey = NULL;
221
167k
  struct sshbuf *state = NULL;
222
167k
  struct kex_params kex_params;
223
167k
  const char *proposal[PROPOSAL_MAX] = { KEX_CLIENT };
224
167k
  char *myproposal[PROPOSAL_MAX] = {0};
225
167k
  char *keyname = NULL;
226
167k
  int i, r;
227
228
167k
  ts->cin = ts->sin = NULL;
229
167k
  if (ts->c_template != NULL &&
230
83.6k
      (ts->cin = sshbuf_fromb(ts->c_template)) == NULL)
231
0
    abort();
232
167k
  if (ts->s_template != NULL &&
233
83.6k
      (ts->sin = sshbuf_fromb(ts->s_template)) == NULL)
234
0
    abort();
235
236
167k
  pubkey = get_pubkey(st, keytype);
237
167k
  privkey = get_privkey(st, keytype);
238
167k
  keyname = xstrdup(sshkey_ssh_name(privkey));
239
167k
  if (ts->cin != NULL) {
240
83.6k
    debug_f("%s %s clobber client %zu", kex, keyname,
241
83.6k
        sshbuf_len(ts->cin));
242
83.6k
  } else if (ts->sin != NULL) {
243
83.6k
    debug_f("%s %s clobber server %zu", kex, keyname,
244
83.6k
        sshbuf_len(ts->sin));
245
83.6k
  } else
246
0
    debug_f("%s %s noclobber", kex, keyname);
247
248
167k
  if (build_proposal_side(myproposal, proposal, kex, keyname,
249
167k
      ov ? ov->cipher : NULL,
250
167k
      ov ? ov->mac : NULL,
251
167k
      ov ? ov->comp : NULL) != 0) {
252
0
    error_f("build proposal");
253
0
    r = SSH_ERR_ALLOC_FAIL;
254
0
    goto fail;
255
0
  }
256
167k
  memcpy(kex_params.proposal, myproposal, sizeof(myproposal));
257
167k
  if ((r = ssh_init(&client, 0, &kex_params)) != 0) {
258
0
    error_fr(r, "init client");
259
0
    goto fail;
260
0
  }
261
167k
  if ((r = ssh_init(&server, 1, &kex_params)) != 0) {
262
0
    error_fr(r, "init server");
263
0
    goto fail;
264
0
  }
265
167k
  if ((r = ssh_add_hostkey(server, privkey)) != 0 ||
266
167k
      (r = ssh_add_hostkey(client, pubkey)) != 0) {
267
0
    error_fr(r, "add hostkeys");
268
0
    goto fail;
269
0
  }
270
167k
  if ((r = run_kex(ts, client, server)) != 0) {
271
167k
    error_fr(r, "kex");
272
167k
    goto fail;
273
167k
  }
274
  /* XXX rekex, set_state, etc */
275
167k
 fail:
276
1.84M
  for (i = 0; i < PROPOSAL_MAX; i++)
277
1.67M
    free(myproposal[i]);
278
167k
  sshbuf_free(ts->sin);
279
167k
  sshbuf_free(ts->cin);
280
167k
  sshbuf_free(state);
281
167k
  ssh_free(client);
282
167k
  ssh_free(server);
283
167k
  free(keyname);
284
167k
  return r;
285
167k
}
286
287
static int
288
prepare_key(struct shared_state *st, int kt, int bits)
289
3
{
290
3
  const char *pubstr = NULL;
291
3
  const char *privstr = NULL;
292
3
  char *tmp, *cp;
293
3
  struct sshkey *privkey = NULL, *pubkey = NULL;
294
3
  struct sshbuf *b = NULL;
295
3
  int r;
296
297
3
  switch (kt) {
298
1
  case KEY_RSA:
299
1
    pubstr = PUB_RSA;
300
1
    privstr = PRIV_RSA;
301
1
    break;
302
1
  case KEY_ECDSA:
303
1
    pubstr = PUB_ECDSA;
304
1
    privstr = PRIV_ECDSA;
305
1
    break;
306
1
  case KEY_ED25519:
307
1
    pubstr = PUB_ED25519;
308
1
    privstr = PRIV_ED25519;
309
1
    break;
310
0
  default:
311
0
    abort();
312
3
  }
313
3
  if ((b = sshbuf_from(privstr, strlen(privstr))) == NULL)
314
0
    abort();
315
3
  if ((r = sshkey_parse_private_fileblob(b, "", &privkey, NULL)) != 0) {
316
0
    error_fr(r, "priv %d", kt);
317
0
    abort();
318
0
  }
319
3
  sshbuf_free(b);
320
3
  tmp = cp = xstrdup(pubstr);
321
3
  if ((pubkey = sshkey_new(KEY_UNSPEC)) == NULL)
322
0
    abort();
323
3
  if ((r = sshkey_read(pubkey, &cp)) != 0) {
324
0
    error_fr(r, "pub %d", kt);
325
0
    abort();
326
0
  }
327
3
  free(tmp);
328
329
3
  store_key(st, pubkey, privkey);
330
3
  return 0;
331
3
}
332
333
#if defined(STANDALONE)
334
335
#if 0 /* use this if generating new keys to embed above */
336
static int
337
prepare_key(struct shared_state *st, int keytype, int bits)
338
{
339
  struct sshkey *privkey = NULL, *pubkey = NULL;
340
  int r;
341
342
  if ((r = sshkey_generate(keytype, bits, &privkey)) != 0) {
343
    error_fr(r, "generate");
344
    abort();
345
  }
346
  if ((r = sshkey_from_private(privkey, &pubkey)) != 0) {
347
    error_fr(r, "make pubkey");
348
    abort();
349
  }
350
  store_key(st, pubkey, privkey);
351
  return 0;
352
}
353
#endif
354
355
int main(void)
356
{
357
  static struct shared_state *st;
358
  struct test_state *ts;
359
  const int keytypes[] = { KEY_RSA, KEY_ECDSA, KEY_ED25519, -1 };
360
  static const char * const kextypes[] = {
361
    "mlkem768x25519-sha256",
362
    "sntrup761x25519-sha512@openssh.com",
363
    "curve25519-sha256@libssh.org",
364
    "ecdh-sha2-nistp256",
365
    "diffie-hellman-group1-sha1",
366
    "diffie-hellman-group-exchange-sha1",
367
    NULL,
368
  };
369
  int i, j;
370
  char *path;
371
  FILE *f;
372
373
  log_init("kex_fuzz", SYSLOG_LEVEL_DEBUG3, SYSLOG_FACILITY_AUTH, 1);
374
375
  if (st == NULL) {
376
    st = (struct shared_state *)xcalloc(1, sizeof(*st));
377
    prepare_keys(st);
378
  }
379
  /* Run each kex method for each key and save client/server packets */
380
  for (i = 0; keytypes[i] != -1; i++) {
381
    for (j = 0; kextypes[j] != NULL; j++) {
382
      ts = (struct test_state *)xcalloc(1, sizeof(*ts));
383
      ts->smsgs = sshbuf_new();
384
      ts->cmsgs = sshbuf_new();
385
      do_kex_with_key(st, ts, kextypes[j], keytypes[i], NULL);
386
      xasprintf(&path, "S2C-%s-%s",
387
          kextypes[j], sshkey_type(st->pubkeys[keytypes[i]]));
388
      debug_f("%s", path);
389
      if ((f = fopen(path, "wb+")) == NULL)
390
        abort();
391
      if (fwrite(sshbuf_ptr(ts->smsgs), 1,
392
          sshbuf_len(ts->smsgs), f) != sshbuf_len(ts->smsgs))
393
        abort();
394
      fclose(f);
395
      free(path);
396
      //sshbuf_dump(ts->smsgs, stderr);
397
      xasprintf(&path, "C2S-%s-%s",
398
          kextypes[j], sshkey_type(st->pubkeys[keytypes[i]]));
399
      debug_f("%s", path);
400
      if ((f = fopen(path, "wb+")) == NULL)
401
        abort();
402
      if (fwrite(sshbuf_ptr(ts->cmsgs), 1,
403
          sshbuf_len(ts->cmsgs), f) != sshbuf_len(ts->cmsgs))
404
        abort();
405
      fclose(f);
406
      free(path);
407
      //sshbuf_dump(ts->cmsgs, stderr);
408
      sshbuf_free(ts->smsgs);
409
      sshbuf_free(ts->cmsgs);
410
      free(ts);
411
    }
412
  }
413
  for (i = 0; keytypes[i] != -1; i++) {
414
    xasprintf(&path, "%s.priv",
415
        sshkey_type(st->privkeys[keytypes[i]]));
416
    debug_f("%s", path);
417
    if (sshkey_save_private(st->privkeys[keytypes[i]], path,
418
        "", "", SSHKEY_PRIVATE_OPENSSH, NULL, 0) != 0)
419
      abort();
420
    free(path);
421
    xasprintf(&path, "%s.pub",
422
        sshkey_type(st->pubkeys[keytypes[i]]));
423
    debug_f("%s", path);
424
    if (sshkey_save_public(st->pubkeys[keytypes[i]], path, "") != 0)
425
      abort();
426
    free(path);
427
  }
428
}
429
#else /* !STANDALONE */
430
static void
431
do_kex(struct shared_state *st, struct test_state *ts, const char *kex)
432
37.1k
{
433
37.1k
  do_kex_with_key(st, ts, kex, KEY_RSA, NULL);
434
37.1k
  do_kex_with_key(st, ts, kex, KEY_ECDSA, NULL);
435
37.1k
  do_kex_with_key(st, ts, kex, KEY_ED25519, NULL);
436
37.1k
}
437
438
/*
439
 * Run one full KEX per variant on the cheapest kex+key combo
440
 * (curve25519 + ed25519) so cipher/MAC/compression/mismatch paths get
441
 * exercised once per fuzz input without multiplying total attempts.
442
 */
443
static void
444
kex_extras(struct shared_state *st, struct test_state *ts)
445
6.19k
{
446
6.19k
  static const char * const ciphers[] = {
447
6.19k
    "chacha20-poly1305@openssh.com",
448
6.19k
    "aes256-gcm@openssh.com",
449
6.19k
    "aes128-ctr",
450
6.19k
    "aes256-ctr",
451
6.19k
    NULL,
452
6.19k
  };
453
6.19k
  static const char * const macs[] = {
454
6.19k
    "hmac-sha2-256-etm@openssh.com",
455
6.19k
    "hmac-sha2-512-etm@openssh.com",
456
6.19k
    "umac-128-etm@openssh.com",
457
6.19k
    NULL,
458
6.19k
  };
459
6.19k
  static const char * const comps[] = {
460
6.19k
    "zlib@openssh.com",
461
6.19k
    "zlib",
462
6.19k
    NULL,
463
6.19k
  };
464
6.19k
  const char *kex = "curve25519-sha256@libssh.org";
465
6.19k
  int i;
466
467
30.9k
  for (i = 0; ciphers[i] != NULL; i++) {
468
24.7k
    struct kex_overrides ov = { 0 };
469
24.7k
    ov.cipher = ciphers[i];
470
24.7k
    do_kex_with_key(st, ts, kex, KEY_ED25519, &ov);
471
24.7k
  }
472
24.7k
  for (i = 0; macs[i] != NULL; i++) {
473
18.5k
    struct kex_overrides ov = { 0 };
474
18.5k
    ov.cipher = "aes128-ctr";
475
18.5k
    ov.mac = macs[i];
476
18.5k
    do_kex_with_key(st, ts, kex, KEY_ED25519, &ov);
477
18.5k
  }
478
18.5k
  for (i = 0; comps[i] != NULL; i++) {
479
12.3k
    struct kex_overrides ov = { 0 };
480
12.3k
    ov.comp = comps[i];
481
12.3k
    do_kex_with_key(st, ts, kex, KEY_ED25519, &ov);
482
12.3k
  }
483
6.19k
}
484
485
static void
486
kex_tests(struct shared_state *st, struct test_state *ts)
487
6.19k
{
488
6.19k
  do_kex(st, ts, "mlkem768x25519-sha256");
489
6.19k
  do_kex(st, ts, "sntrup761x25519-sha512@openssh.com");
490
6.19k
  do_kex(st, ts, "curve25519-sha256@libssh.org");
491
6.19k
  do_kex(st, ts, "ecdh-sha2-nistp256");
492
6.19k
  do_kex(st, ts, "diffie-hellman-group1-sha1");
493
6.19k
  do_kex(st, ts, "diffie-hellman-group-exchange-sha1");
494
6.19k
  kex_extras(st, ts);
495
6.19k
}
496
497
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
498
3.09k
{
499
3.09k
  static struct shared_state *st;
500
3.09k
  struct test_state *ts;
501
3.09k
  u_char crbuf[SSH_MAX_PRE_BANNER_LINES * 4];
502
3.09k
  u_char zbuf[4096] = {0};
503
3.09k
  static LogLevel loglevel = SYSLOG_LEVEL_INFO;
504
505
3.09k
  if (st == NULL) {
506
1
    if (getenv("DEBUG") != NULL || getenv("KEX_FUZZ_DEBUG") != NULL)
507
0
      loglevel = SYSLOG_LEVEL_DEBUG3;
508
1
    log_init("kex_fuzz",
509
1
        loglevel, SYSLOG_FACILITY_AUTH, 1);
510
1
    st = (struct shared_state *)xcalloc(1, sizeof(*st));
511
1
    prepare_keys(st);
512
1
  }
513
514
  /* Ensure that we can complete (fail) banner exchange at least */
515
3.09k
  memset(crbuf, '\n', sizeof(crbuf));
516
517
3.09k
  ts = (struct test_state *)xcalloc(1, sizeof(*ts));
518
3.09k
  if ((ts->s_template = sshbuf_new()) == NULL ||
519
3.09k
      sshbuf_put(ts->s_template, data, size) != 0 ||
520
3.09k
      sshbuf_put(ts->s_template, crbuf, sizeof(crbuf)) != 0 ||
521
3.09k
      sshbuf_put(ts->s_template, zbuf, sizeof(zbuf)) != 0)
522
0
    abort();
523
3.09k
  kex_tests(st, ts);
524
3.09k
  sshbuf_free(ts->s_template);
525
3.09k
  free(ts);
526
527
3.09k
  ts = (struct test_state *)xcalloc(1, sizeof(*ts));
528
3.09k
  if ((ts->c_template = sshbuf_new()) == NULL ||
529
3.09k
      sshbuf_put(ts->c_template, data, size) != 0 ||
530
3.09k
      sshbuf_put(ts->c_template, crbuf, sizeof(crbuf)) != 0 ||
531
3.09k
      sshbuf_put(ts->c_template, zbuf, sizeof(zbuf)) != 0)
532
0
    abort();
533
3.09k
  kex_tests(st, ts);
534
3.09k
  sshbuf_free(ts->c_template);
535
3.09k
  free(ts);
536
537
3.09k
  return 0;
538
3.09k
}
539
#endif /* STANDALONE */
540
} /* extern "C" */