Coverage Report

Created: 2026-09-03 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssh/auth2-pubkeyfile.c
Line
Count
Source
1
/* $OpenBSD: auth2-pubkeyfile.c,v 1.8 2026/04/02 07:48:13 djm Exp $ */
2
/*
3
 * Copyright (c) 2000 Markus Friedl.  All rights reserved.
4
 * Copyright (c) 2010 Damien Miller.  All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions
8
 * are met:
9
 * 1. Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 * 2. Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
27
#include "includes.h"
28
29
#include <sys/types.h>
30
#include <sys/stat.h>
31
32
#include <stdlib.h>
33
#include <errno.h>
34
#include <fcntl.h>
35
#include <pwd.h>
36
#include <stdio.h>
37
#include <stdarg.h>
38
#include <string.h>
39
#include <time.h>
40
#include <unistd.h>
41
42
#include "ssh.h"
43
#include "log.h"
44
#include "misc.h"
45
#include "sshkey.h"
46
#include "digest.h"
47
#include "hostfile.h"
48
#include "auth.h"
49
#include "auth-options.h"
50
#include "authfile.h"
51
#include "match.h"
52
#include "ssherr.h"
53
#include "xmalloc.h"
54
55
int
56
auth_authorise_keyopts(struct passwd *pw, struct sshauthopt *opts,
57
    int allow_cert_authority, const char *remote_ip, const char *remote_host,
58
    const char *loc)
59
2.21k
{
60
2.21k
  time_t now = time(NULL);
61
2.21k
  char buf[64];
62
63
  /*
64
   * Check keys/principals file expiry time.
65
   * NB. validity interval in certificate is handled elsewhere.
66
   */
67
2.21k
  if (opts->valid_before && now > 0 &&
68
432
      opts->valid_before < (uint64_t)now) {
69
76
    format_absolute_time(opts->valid_before, buf, sizeof(buf));
70
76
    debug("%s: entry expired at %s", loc, buf);
71
76
    auth_debug_add("%s: entry expired at %s", loc, buf);
72
76
    return -1;
73
76
  }
74
  /* Consistency checks */
75
2.14k
  if (opts->cert_principals != NULL && !opts->cert_authority) {
76
1
    debug("%s: principals on non-CA key", loc);
77
1
    auth_debug_add("%s: principals on non-CA key", loc);
78
    /* deny access */
79
1
    return -1;
80
1
  }
81
  /* cert-authority flag isn't valid in authorized_principals files */
82
2.13k
  if (!allow_cert_authority && opts->cert_authority) {
83
0
    debug("%s: cert-authority flag invalid here", loc);
84
0
    auth_debug_add("%s: cert-authority flag invalid here", loc);
85
    /* deny access */
86
0
    return -1;
87
0
  }
88
89
  /* Perform from= checks */
90
2.13k
  if (opts->required_from_host_keys != NULL) {
91
937
    switch (match_host_and_ip(remote_host, remote_ip,
92
937
        opts->required_from_host_keys )) {
93
53
    case 1:
94
      /* Host name matches. */
95
53
      break;
96
150
    case -1:
97
150
    default:
98
150
      debug("%s: invalid from criteria", loc);
99
150
      auth_debug_add("%s: invalid from criteria", loc);
100
      /* FALLTHROUGH */
101
884
    case 0:
102
884
      logit("%s: Authentication tried for %.100s with "
103
884
          "correct key but not from a permitted "
104
884
          "host (host=%.200s, ip=%.200s, required=%.200s).",
105
884
          loc, pw->pw_name, remote_host, remote_ip,
106
884
          opts->required_from_host_keys);
107
884
      auth_debug_add("%s: Your host '%.200s' is not "
108
884
          "permitted to use this key for login.",
109
884
          loc, remote_host);
110
      /* deny access */
111
884
      return -1;
112
937
    }
113
937
  }
114
  /* Check source-address restriction from certificate */
115
1.25k
  if (opts->required_from_host_cert != NULL) {
116
0
    switch (addr_match_cidr_list(remote_ip,
117
0
        opts->required_from_host_cert)) {
118
0
    case 1:
119
      /* accepted */
120
0
      break;
121
0
    case -1:
122
0
    default:
123
      /* invalid */
124
0
      error("%s: Certificate source-address invalid", loc);
125
      /* FALLTHROUGH */
126
0
    case 0:
127
0
      logit("%s: Authentication tried for %.100s with valid "
128
0
          "certificate but not from a permitted source "
129
0
          "address (%.200s).", loc, pw->pw_name, remote_ip);
130
0
      auth_debug_add("%s: Your address '%.200s' is not "
131
0
          "permitted to use this certificate for login.",
132
0
          loc, remote_ip);
133
0
      return -1;
134
0
    }
135
0
  }
136
  /*
137
   *
138
   * XXX this is spammy. We should report remotely only for keys
139
   *     that are successful in actual auth attempts, and not PK_OK
140
   *     tests.
141
   */
142
1.25k
  auth_log_authopts(loc, opts, 1);
143
144
1.25k
  return 0;
145
1.25k
}
146
147
static int
148
match_principals_option(const char *principal_list, struct sshkey_cert *cert)
149
114
{
150
114
  char *list, *olist, *entry;
151
114
  u_int i;
152
153
114
  olist = list = xstrdup(principal_list);
154
1.99k
  for (;;) {
155
1.99k
    if ((entry = strsep(&list, ",")) == NULL || *entry == '\0')
156
110
      break;
157
5.65k
    for (i = 0; i < cert->nprincipals; i++) {
158
3.77k
      if (strcmp(entry, cert->principals[i]) == 0) {
159
4
        debug3("matched principal from key i"
160
4
            "options \"%.100s\"", entry);
161
4
        free(olist);
162
4
        return 1;
163
4
      }
164
3.77k
    }
165
1.88k
  }
166
110
  free(olist);
167
110
  return 0;
168
114
}
169
170
/*
171
 * Process a single authorized_principals format line. Returns 0 and sets
172
 * authoptsp is principal is authorised, -1 otherwise. "loc" is used as a
173
 * log preamble for file/line information.
174
 */
175
int
176
auth_check_principals_line(char *cp, const struct sshkey_cert *cert,
177
    const char *loc, struct sshauthopt **authoptsp)
178
0
{
179
0
  u_int i, found = 0;
180
0
  char *ep, *line_opts;
181
0
  const char *reason = NULL;
182
0
  struct sshauthopt *opts = NULL;
183
184
0
  if (authoptsp != NULL)
185
0
    *authoptsp = NULL;
186
187
  /* Trim trailing whitespace. */
188
0
  ep = cp + strlen(cp) - 1;
189
0
  while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t'))
190
0
    *ep-- = '\0';
191
192
  /*
193
   * If the line has internal whitespace then assume it has
194
   * key options.
195
   */
196
0
  line_opts = NULL;
197
0
  if ((ep = strrchr(cp, ' ')) != NULL ||
198
0
      (ep = strrchr(cp, '\t')) != NULL) {
199
0
    for (; *ep == ' ' || *ep == '\t'; ep++)
200
0
      ;
201
0
    line_opts = cp;
202
0
    cp = ep;
203
0
  }
204
0
  if ((opts = sshauthopt_parse(line_opts, &reason)) == NULL) {
205
0
    debug("%s: bad principals options: %s", loc, reason);
206
0
    auth_debug_add("%s: bad principals options: %s", loc, reason);
207
0
    return -1;
208
0
  }
209
  /* Check principals in cert against those on line */
210
0
  for (i = 0; i < cert->nprincipals; i++) {
211
0
    if (strcmp(cp, cert->principals[i]) != 0)
212
0
      continue;
213
0
    debug3("%s: matched principal \"%.100s\"",
214
0
        loc, cert->principals[i]);
215
0
    found = 1;
216
0
  }
217
0
  if (found && authoptsp != NULL) {
218
0
    *authoptsp = opts;
219
0
    opts = NULL;
220
0
  }
221
0
  sshauthopt_free(opts);
222
0
  return found ? 0 : -1;
223
0
}
224
225
int
226
auth_process_principals(FILE *f, const char *file,
227
    const struct sshkey_cert *cert, struct sshauthopt **authoptsp)
228
0
{
229
0
  char loc[256], *line = NULL, *cp, *ep;
230
0
  size_t linesize = 0;
231
0
  u_long linenum = 0, nonblank = 0;
232
0
  u_int found_principal = 0;
233
234
0
  if (authoptsp != NULL)
235
0
    *authoptsp = NULL;
236
237
0
  while (getline(&line, &linesize, f) != -1) {
238
0
    linenum++;
239
    /* Always consume entire input */
240
0
    if (found_principal)
241
0
      continue;
242
243
    /* Skip leading whitespace. */
244
0
    for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
245
0
      ;
246
    /* Skip blank and comment lines. */
247
0
    if ((ep = strchr(cp, '#')) != NULL)
248
0
      *ep = '\0';
249
0
    if (!*cp || *cp == '\n')
250
0
      continue;
251
252
0
    nonblank++;
253
0
    snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
254
0
    if (auth_check_principals_line(cp, cert, loc, authoptsp) == 0)
255
0
      found_principal = 1;
256
0
  }
257
0
  debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
258
0
  free(line);
259
0
  return found_principal;
260
0
}
261
262
/*
263
 * Check a single line of an authorized_keys-format file. Returns 0 if key
264
 * matches, -1 otherwise. Will return key/cert options via *authoptsp
265
 * on success. "loc" is used as file/line location in log messages.
266
 */
267
int
268
auth_check_authkey_line(struct passwd *pw, struct sshkey *key,
269
    char *cp, const char *remote_ip, const char *remote_host, const char *loc,
270
    struct sshauthopt **authoptsp)
271
6.46k
{
272
6.46k
  int want_keytype = sshkey_is_cert(key) ? KEY_UNSPEC : key->type;
273
6.46k
  struct sshkey *found = NULL;
274
6.46k
  struct sshauthopt *keyopts = NULL, *certopts = NULL, *finalopts = NULL;
275
6.46k
  char *key_options = NULL, *fp = NULL;
276
6.46k
  const char *reason = NULL;
277
6.46k
  int ret = -1;
278
279
6.46k
  if (authoptsp != NULL)
280
6.46k
    *authoptsp = NULL;
281
282
6.46k
  if ((found = sshkey_new(want_keytype)) == NULL) {
283
0
    debug3_f("keytype %d failed", want_keytype);
284
0
    goto out;
285
0
  }
286
287
  /* XXX djm: peek at key type in line and skip if unwanted */
288
289
6.46k
  if (sshkey_read(found, &cp) != 0) {
290
    /* no key?  check for options */
291
6.46k
    debug2("%s: check options: '%s'", loc, cp);
292
6.46k
    key_options = cp;
293
6.46k
    if (sshkey_advance_past_options(&cp) != 0) {
294
16
      reason = "invalid key option string";
295
16
      goto fail_reason;
296
16
    }
297
6.44k
    skip_space(&cp);
298
6.44k
    if (sshkey_read(found, &cp) != 0) {
299
      /* still no key?  advance to next line*/
300
816
      debug2("%s: advance: '%s'", loc, cp);
301
816
      goto out;
302
816
    }
303
6.44k
  }
304
  /* Parse key options now; we need to know if this is a CA key */
305
5.62k
  if ((keyopts = sshauthopt_parse(key_options, &reason)) == NULL) {
306
1.79k
    debug("%s: bad key options: %s", loc, reason);
307
1.79k
    auth_debug_add("%s: bad key options: %s", loc, reason);
308
1.79k
    goto out;
309
1.79k
  }
310
  /* Ignore keys that don't match or incorrectly marked as CAs */
311
3.83k
  if (sshkey_is_cert(key)) {
312
    /* Certificate; check signature key against CA */
313
1.91k
    if (!sshkey_equal(found, key->cert->signature_key) ||
314
1.91k
        !keyopts->cert_authority)
315
1.61k
      goto out;
316
1.91k
  } else {
317
    /* Plain key: check it against key found in file */
318
1.91k
    if (!sshkey_equal(found, key) || keyopts->cert_authority)
319
301
      goto out;
320
1.91k
  }
321
322
  /* We have a candidate key, perform authorisation checks */
323
1.91k
  if ((fp = sshkey_fingerprint(found,
324
1.91k
      SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL)
325
0
    fatal_f("fingerprint failed");
326
327
1.91k
  debug("%s: matching %s found: %s %s", loc,
328
1.91k
      sshkey_is_cert(key) ? "CA" : "key", sshkey_type(found), fp);
329
330
1.91k
  if (auth_authorise_keyopts(pw, keyopts,
331
1.91k
      sshkey_is_cert(key), remote_ip, remote_host, loc) != 0) {
332
961
    reason = "Refused by key options";
333
961
    goto fail_reason;
334
961
  }
335
  /* That's all we need for plain keys. */
336
954
  if (!sshkey_is_cert(key)) {
337
653
    verbose("Accepted key %s %s found at %s",
338
653
        sshkey_type(found), fp, loc);
339
653
    finalopts = keyopts;
340
653
    keyopts = NULL;
341
653
    goto success;
342
653
  }
343
344
  /*
345
   * Additional authorisation for certificates.
346
   */
347
348
  /* Parse and check options present in certificate */
349
301
  if ((certopts = sshauthopt_from_cert(key)) == NULL) {
350
0
    reason = "Invalid certificate options";
351
0
    goto cert_fail_reason;
352
0
  }
353
301
  if (auth_authorise_keyopts(pw, certopts, 0,
354
301
      remote_ip, remote_host, loc) != 0) {
355
0
    reason = "Refused by certificate options";
356
0
    goto cert_fail_reason;
357
0
  }
358
301
  if ((finalopts = sshauthopt_merge(keyopts, certopts, &reason)) == NULL)
359
0
    goto cert_fail_reason;
360
361
  /*
362
   * If the user has specified a list of principals as
363
   * a key option, then prefer that list to matching
364
   * their username in the certificate principals list.
365
   */
366
301
  if (keyopts->cert_principals != NULL &&
367
114
      !match_principals_option(keyopts->cert_principals, key->cert)) {
368
110
    reason = "Certificate does not contain an authorized principal";
369
110
    goto cert_fail_reason;
370
110
  }
371
191
  if (sshkey_cert_check_authority_now(key, 0, 0,
372
191
      keyopts->cert_principals == NULL ? pw->pw_name : NULL,
373
191
      &reason) != 0)
374
187
    goto cert_fail_reason;
375
376
4
  verbose("Accepted certificate ID \"%s\" (serial %llu) "
377
4
      "signed by CA %s %s found at %s",
378
4
      key->cert->key_id,
379
4
      (unsigned long long)key->cert->serial,
380
4
      sshkey_type(found), fp, loc);
381
382
657
 success:
383
657
  if (finalopts == NULL)
384
0
    fatal_f("internal error: missing options");
385
657
  if (authoptsp != NULL) {
386
657
    *authoptsp = finalopts;
387
657
    finalopts = NULL;
388
657
  }
389
  /* success */
390
657
  ret = 0;
391
657
  goto out;
392
393
297
 cert_fail_reason:
394
297
  error("Refusing certificate ID \"%s\" serial=%llu "
395
297
      "signed by %s CA %s via %s: %s", key->cert->key_id,
396
297
      (unsigned long long)key->cert->serial,
397
297
      sshkey_type(key->cert->signature_key), fp, loc, reason);
398
297
  auth_debug_add("Refused Certificate ID \"%s\" serial=%llu: %s",
399
297
      key->cert->key_id, (unsigned long long)key->cert->serial, reason);
400
297
  goto out;
401
402
977
 fail_reason:
403
977
  error("%s at %s", reason, loc);
404
977
  auth_debug_add("%s", reason);
405
6.46k
 out:
406
6.46k
  free(fp);
407
6.46k
  sshauthopt_free(keyopts);
408
6.46k
  sshauthopt_free(certopts);
409
6.46k
  sshauthopt_free(finalopts);
410
6.46k
  sshkey_free(found);
411
6.46k
  return ret;
412
977
}
413
414
/*
415
 * Checks whether key is allowed in authorized_keys-format file,
416
 * returns 1 if the key is allowed or 0 otherwise.
417
 */
418
int
419
auth_check_authkeys_file(struct passwd *pw, FILE *f, char *file,
420
    struct sshkey *key, const char *remote_ip,
421
    const char *remote_host, struct sshauthopt **authoptsp)
422
0
{
423
0
  char *cp, *line = NULL, loc[256];
424
0
  size_t linesize = 0;
425
0
  int found_key = 0;
426
0
  u_long linenum = 0, nonblank = 0;
427
428
0
  if (authoptsp != NULL)
429
0
    *authoptsp = NULL;
430
431
0
  while (getline(&line, &linesize, f) != -1) {
432
0
    linenum++;
433
    /* Always consume entire file */
434
0
    if (found_key)
435
0
      continue;
436
437
    /* Skip leading whitespace, empty and comment lines. */
438
0
    cp = line;
439
0
    skip_space(&cp);
440
0
    if (!*cp || *cp == '\n' || *cp == '#')
441
0
      continue;
442
443
0
    nonblank++;
444
0
    snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
445
0
    if (auth_check_authkey_line(pw, key, cp,
446
0
        remote_ip, remote_host, loc, authoptsp) == 0)
447
0
      found_key = 1;
448
0
  }
449
0
  free(line);
450
0
  debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
451
0
  return found_key;
452
0
}
453
454
static FILE *
455
auth_openfile(const char *file, struct passwd *pw, int strict_modes,
456
    int log_missing, char *file_type)
457
0
{
458
0
  char line[1024];
459
0
  struct stat st;
460
0
  int fd;
461
0
  FILE *f;
462
463
0
  if ((fd = open(file, O_RDONLY|O_NONBLOCK)) == -1) {
464
0
    if (errno != ENOENT) {
465
0
      logit("Could not open user '%s' %s '%s': %s",
466
0
          pw->pw_name, file_type, file, strerror(errno));
467
0
    } else if (log_missing) {
468
0
      debug("Could not open user '%s' %s '%s': %s",
469
0
          pw->pw_name, file_type, file, strerror(errno));
470
0
    }
471
0
    return NULL;
472
0
  }
473
474
0
  if (fstat(fd, &st) == -1) {
475
0
    close(fd);
476
0
    return NULL;
477
0
  }
478
0
  if (!S_ISREG(st.st_mode)) {
479
0
    logit("User '%s' %s '%s' is not a regular file",
480
0
        pw->pw_name, file_type, file);
481
0
    close(fd);
482
0
    return NULL;
483
0
  }
484
0
  unset_nonblock(fd);
485
0
  if ((f = fdopen(fd, "r")) == NULL) {
486
0
    close(fd);
487
0
    return NULL;
488
0
  }
489
0
  if (strict_modes &&
490
0
      safe_path_fd(fileno(f), file, pw, line, sizeof(line)) != 0) {
491
0
    fclose(f);
492
0
    logit("Authentication refused: %s", line);
493
0
    auth_debug_add("Ignored %s: %s", file_type, line);
494
0
    return NULL;
495
0
  }
496
497
0
  return f;
498
0
}
499
500
501
FILE *
502
auth_openkeyfile(const char *file, struct passwd *pw, int strict_modes)
503
0
{
504
0
  return auth_openfile(file, pw, strict_modes, 1, "authorized keys");
505
0
}
506
507
FILE *
508
auth_openprincipals(const char *file, struct passwd *pw, int strict_modes)
509
0
{
510
0
  return auth_openfile(file, pw, strict_modes, 0,
511
0
      "authorized principals");
512
0
}
513