Annotation of src/usr.bin/ssh/auth2-pubkeyfile.c, Revision 1.2
1.2 ! djm 1: /* $OpenBSD: auth2-pubkeyfile.c,v 1.1 2022/05/27 05:02:46 djm Exp $ */
1.1 djm 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:
28: #include <sys/types.h>
29: #include <sys/stat.h>
30:
31: #include <stdlib.h>
32: #include <errno.h>
33: #include <fcntl.h>
34: #include <pwd.h>
35: #include <stdio.h>
36: #include <stdarg.h>
37: #include <string.h>
38: #include <time.h>
39: #include <unistd.h>
40:
41: #include "ssh.h"
42: #include "log.h"
43: #include "misc.h"
44: #include "compat.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:
54: int
55: auth_authorise_keyopts(struct passwd *pw, struct sshauthopt *opts,
56: int allow_cert_authority, const char *remote_ip, const char *remote_host,
57: const char *loc)
58: {
59: time_t now = time(NULL);
60: char buf[64];
61:
62: /*
63: * Check keys/principals file expiry time.
64: * NB. validity interval in certificate is handled elsewhere.
65: */
66: if (opts->valid_before && now > 0 &&
67: opts->valid_before < (uint64_t)now) {
68: format_absolute_time(opts->valid_before, buf, sizeof(buf));
69: debug("%s: entry expired at %s", loc, buf);
70: auth_debug_add("%s: entry expired at %s", loc, buf);
71: return -1;
72: }
73: /* Consistency checks */
74: if (opts->cert_principals != NULL && !opts->cert_authority) {
75: debug("%s: principals on non-CA key", loc);
76: auth_debug_add("%s: principals on non-CA key", loc);
77: /* deny access */
78: return -1;
79: }
80: /* cert-authority flag isn't valid in authorized_principals files */
81: if (!allow_cert_authority && opts->cert_authority) {
82: debug("%s: cert-authority flag invalid here", loc);
83: auth_debug_add("%s: cert-authority flag invalid here", loc);
84: /* deny access */
85: return -1;
86: }
87:
88: /* Perform from= checks */
89: if (opts->required_from_host_keys != NULL) {
90: switch (match_host_and_ip(remote_host, remote_ip,
91: opts->required_from_host_keys )) {
92: case 1:
93: /* Host name matches. */
94: break;
95: case -1:
96: default:
97: debug("%s: invalid from criteria", loc);
98: auth_debug_add("%s: invalid from criteria", loc);
99: /* FALLTHROUGH */
100: case 0:
101: logit("%s: Authentication tried for %.100s with "
102: "correct key but not from a permitted "
103: "host (host=%.200s, ip=%.200s, required=%.200s).",
104: loc, pw->pw_name, remote_host, remote_ip,
105: opts->required_from_host_keys);
106: auth_debug_add("%s: Your host '%.200s' is not "
107: "permitted to use this key for login.",
108: loc, remote_host);
109: /* deny access */
110: return -1;
111: }
112: }
113: /* Check source-address restriction from certificate */
114: if (opts->required_from_host_cert != NULL) {
115: switch (addr_match_cidr_list(remote_ip,
116: opts->required_from_host_cert)) {
117: case 1:
118: /* accepted */
119: break;
120: case -1:
121: default:
122: /* invalid */
123: error("%s: Certificate source-address invalid", loc);
124: /* FALLTHROUGH */
125: case 0:
126: logit("%s: Authentication tried for %.100s with valid "
127: "certificate but not from a permitted source "
128: "address (%.200s).", loc, pw->pw_name, remote_ip);
129: auth_debug_add("%s: Your address '%.200s' is not "
130: "permitted to use this certificate for login.",
131: loc, remote_ip);
132: return -1;
133: }
134: }
135: /*
136: *
137: * XXX this is spammy. We should report remotely only for keys
138: * that are successful in actual auth attempts, and not PK_OK
139: * tests.
140: */
141: auth_log_authopts(loc, opts, 1);
142:
143: return 0;
144: }
145:
146: static int
147: match_principals_option(const char *principal_list, struct sshkey_cert *cert)
148: {
149: char *result;
150: u_int i;
151:
152: /* XXX percent_expand() sequences for authorized_principals? */
153:
154: for (i = 0; i < cert->nprincipals; i++) {
155: if ((result = match_list(cert->principals[i],
156: principal_list, NULL)) != NULL) {
157: debug3("matched principal from key options \"%.100s\"",
158: result);
159: free(result);
160: return 1;
161: }
162: }
163: return 0;
164: }
165:
166: /*
167: * Process a single authorized_principals format line. Returns 0 and sets
168: * authoptsp is principal is authorised, -1 otherwise. "loc" is used as a
169: * log preamble for file/line information.
170: */
171: int
172: auth_check_principals_line(char *cp, const struct sshkey_cert *cert,
173: const char *loc, struct sshauthopt **authoptsp)
174: {
175: u_int i, found = 0;
176: char *ep, *line_opts;
177: const char *reason = NULL;
178: struct sshauthopt *opts = NULL;
179:
180: if (authoptsp != NULL)
181: *authoptsp = NULL;
182:
183: /* Trim trailing whitespace. */
184: ep = cp + strlen(cp) - 1;
185: while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t'))
186: *ep-- = '\0';
187:
188: /*
189: * If the line has internal whitespace then assume it has
190: * key options.
191: */
192: line_opts = NULL;
193: if ((ep = strrchr(cp, ' ')) != NULL ||
194: (ep = strrchr(cp, '\t')) != NULL) {
195: for (; *ep == ' ' || *ep == '\t'; ep++)
196: ;
197: line_opts = cp;
198: cp = ep;
199: }
200: if ((opts = sshauthopt_parse(line_opts, &reason)) == NULL) {
201: debug("%s: bad principals options: %s", loc, reason);
202: auth_debug_add("%s: bad principals options: %s", loc, reason);
203: return -1;
204: }
205: /* Check principals in cert against those on line */
206: for (i = 0; i < cert->nprincipals; i++) {
207: if (strcmp(cp, cert->principals[i]) != 0)
208: continue;
209: debug3("%s: matched principal \"%.100s\"",
210: loc, cert->principals[i]);
211: found = 1;
212: }
213: if (found && authoptsp != NULL) {
214: *authoptsp = opts;
215: opts = NULL;
216: }
217: sshauthopt_free(opts);
218: return found ? 0 : -1;
219: }
220:
221: int
222: auth_process_principals(FILE *f, const char *file,
223: const struct sshkey_cert *cert, struct sshauthopt **authoptsp)
224: {
225: char loc[256], *line = NULL, *cp, *ep;
226: size_t linesize = 0;
227: u_long linenum = 0, nonblank = 0;
228: u_int found_principal = 0;
229:
230: if (authoptsp != NULL)
231: *authoptsp = NULL;
232:
233: while (getline(&line, &linesize, f) != -1) {
234: linenum++;
235: /* Always consume entire input */
236: if (found_principal)
237: continue;
238:
239: /* Skip leading whitespace. */
240: for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
241: ;
242: /* Skip blank and comment lines. */
243: if ((ep = strchr(cp, '#')) != NULL)
244: *ep = '\0';
245: if (!*cp || *cp == '\n')
246: continue;
247:
248: nonblank++;
249: snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
250: if (auth_check_principals_line(cp, cert, loc, authoptsp) == 0)
251: found_principal = 1;
252: }
253: debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
254: free(line);
255: return found_principal;
256: }
257:
258: /*
259: * Check a single line of an authorized_keys-format file. Returns 0 if key
260: * matches, -1 otherwise. Will return key/cert options via *authoptsp
261: * on success. "loc" is used as file/line location in log messages.
262: */
263: int
264: auth_check_authkey_line(struct passwd *pw, struct sshkey *key,
265: char *cp, const char *remote_ip, const char *remote_host, const char *loc,
266: struct sshauthopt **authoptsp)
267: {
268: int want_keytype = sshkey_is_cert(key) ? KEY_UNSPEC : key->type;
269: struct sshkey *found = NULL;
270: struct sshauthopt *keyopts = NULL, *certopts = NULL, *finalopts = NULL;
271: char *key_options = NULL, *fp = NULL;
272: const char *reason = NULL;
273: int ret = -1;
274:
275: if (authoptsp != NULL)
276: *authoptsp = NULL;
277:
278: if ((found = sshkey_new(want_keytype)) == NULL) {
279: debug3_f("keytype %d failed", want_keytype);
280: goto out;
281: }
282:
283: /* XXX djm: peek at key type in line and skip if unwanted */
284:
285: if (sshkey_read(found, &cp) != 0) {
286: /* no key? check for options */
287: debug2("%s: check options: '%s'", loc, cp);
288: key_options = cp;
289: if (sshkey_advance_past_options(&cp) != 0) {
290: reason = "invalid key option string";
291: goto fail_reason;
292: }
293: skip_space(&cp);
294: if (sshkey_read(found, &cp) != 0) {
295: /* still no key? advance to next line*/
296: debug2("%s: advance: '%s'", loc, cp);
297: goto out;
298: }
299: }
300: /* Parse key options now; we need to know if this is a CA key */
301: if ((keyopts = sshauthopt_parse(key_options, &reason)) == NULL) {
302: debug("%s: bad key options: %s", loc, reason);
303: auth_debug_add("%s: bad key options: %s", loc, reason);
304: goto out;
305: }
306: /* Ignore keys that don't match or incorrectly marked as CAs */
307: if (sshkey_is_cert(key)) {
308: /* Certificate; check signature key against CA */
309: if (!sshkey_equal(found, key->cert->signature_key) ||
310: !keyopts->cert_authority)
311: goto out;
312: } else {
313: /* Plain key: check it against key found in file */
314: if (!sshkey_equal(found, key) || keyopts->cert_authority)
315: goto out;
316: }
317:
318: /* We have a candidate key, perform authorisation checks */
319: if ((fp = sshkey_fingerprint(found,
320: SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL)
321: fatal_f("fingerprint failed");
322:
323: debug("%s: matching %s found: %s %s", loc,
324: sshkey_is_cert(key) ? "CA" : "key", sshkey_type(found), fp);
325:
326: if (auth_authorise_keyopts(pw, keyopts,
327: sshkey_is_cert(key), remote_ip, remote_host, loc) != 0) {
328: reason = "Refused by key options";
329: goto fail_reason;
330: }
331: /* That's all we need for plain keys. */
332: if (!sshkey_is_cert(key)) {
333: verbose("Accepted key %s %s found at %s",
334: sshkey_type(found), fp, loc);
335: finalopts = keyopts;
336: keyopts = NULL;
337: goto success;
338: }
339:
340: /*
341: * Additional authorisation for certificates.
342: */
343:
344: /* Parse and check options present in certificate */
345: if ((certopts = sshauthopt_from_cert(key)) == NULL) {
346: reason = "Invalid certificate options";
347: goto fail_reason;
348: }
349: if (auth_authorise_keyopts(pw, certopts, 0,
350: remote_ip, remote_host, loc) != 0) {
351: reason = "Refused by certificate options";
352: goto fail_reason;
353: }
354: if ((finalopts = sshauthopt_merge(keyopts, certopts, &reason)) == NULL)
355: goto fail_reason;
356:
357: /*
358: * If the user has specified a list of principals as
359: * a key option, then prefer that list to matching
360: * their username in the certificate principals list.
361: */
362: if (keyopts->cert_principals != NULL &&
363: !match_principals_option(keyopts->cert_principals, key->cert)) {
364: reason = "Certificate does not contain an authorized principal";
365: goto fail_reason;
366: }
367: if (sshkey_cert_check_authority_now(key, 0, 0, 0,
368: keyopts->cert_principals == NULL ? pw->pw_name : NULL,
369: &reason) != 0)
370: goto fail_reason;
371:
372: verbose("Accepted certificate ID \"%s\" (serial %llu) "
373: "signed by CA %s %s found at %s",
374: key->cert->key_id,
375: (unsigned long long)key->cert->serial,
376: sshkey_type(found), fp, loc);
377:
378: success:
379: if (finalopts == NULL)
380: fatal_f("internal error: missing options");
381: if (authoptsp != NULL) {
382: *authoptsp = finalopts;
383: finalopts = NULL;
384: }
385: /* success */
386: ret = 0;
387: goto out;
388:
389: fail_reason:
390: error("%s", reason);
391: auth_debug_add("%s", reason);
392: out:
393: free(fp);
394: sshauthopt_free(keyopts);
395: sshauthopt_free(certopts);
396: sshauthopt_free(finalopts);
397: sshkey_free(found);
398: return ret;
399: }
400:
401: /*
402: * Checks whether key is allowed in authorized_keys-format file,
403: * returns 1 if the key is allowed or 0 otherwise.
404: */
405: int
406: auth_check_authkeys_file(struct passwd *pw, FILE *f, char *file,
407: struct sshkey *key, const char *remote_ip,
408: const char *remote_host, struct sshauthopt **authoptsp)
409: {
410: char *cp, *line = NULL, loc[256];
411: size_t linesize = 0;
412: int found_key = 0;
413: u_long linenum = 0, nonblank = 0;
414:
415: if (authoptsp != NULL)
416: *authoptsp = NULL;
417:
418: while (getline(&line, &linesize, f) != -1) {
419: linenum++;
420: /* Always consume entire file */
421: if (found_key)
422: continue;
423:
424: /* Skip leading whitespace, empty and comment lines. */
425: cp = line;
426: skip_space(&cp);
427: if (!*cp || *cp == '\n' || *cp == '#')
428: continue;
429:
430: nonblank++;
431: snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum);
432: if (auth_check_authkey_line(pw, key, cp,
433: remote_ip, remote_host, loc, authoptsp) == 0)
434: found_key = 1;
435: }
436: free(line);
437: debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum);
438: return found_key;
439: }
440:
1.2 ! djm 441: static FILE *
! 442: auth_openfile(const char *file, struct passwd *pw, int strict_modes,
! 443: int log_missing, char *file_type)
! 444: {
! 445: char line[1024];
! 446: struct stat st;
! 447: int fd;
! 448: FILE *f;
! 449:
! 450: if ((fd = open(file, O_RDONLY|O_NONBLOCK)) == -1) {
! 451: if (log_missing || errno != ENOENT)
! 452: debug("Could not open %s '%s': %s", file_type, file,
! 453: strerror(errno));
! 454: return NULL;
! 455: }
! 456:
! 457: if (fstat(fd, &st) == -1) {
! 458: close(fd);
! 459: return NULL;
! 460: }
! 461: if (!S_ISREG(st.st_mode)) {
! 462: logit("User %s %s %s is not a regular file",
! 463: pw->pw_name, file_type, file);
! 464: close(fd);
! 465: return NULL;
! 466: }
! 467: unset_nonblock(fd);
! 468: if ((f = fdopen(fd, "r")) == NULL) {
! 469: close(fd);
! 470: return NULL;
! 471: }
! 472: if (strict_modes &&
! 473: safe_path_fd(fileno(f), file, pw, line, sizeof(line)) != 0) {
! 474: fclose(f);
! 475: logit("Authentication refused: %s", line);
! 476: auth_debug_add("Ignored %s: %s", file_type, line);
! 477: return NULL;
! 478: }
! 479:
! 480: return f;
! 481: }
! 482:
! 483:
! 484: FILE *
! 485: auth_openkeyfile(const char *file, struct passwd *pw, int strict_modes)
! 486: {
! 487: return auth_openfile(file, pw, strict_modes, 1, "authorized keys");
! 488: }
! 489:
! 490: FILE *
! 491: auth_openprincipals(const char *file, struct passwd *pw, int strict_modes)
! 492: {
! 493: return auth_openfile(file, pw, strict_modes, 0,
! 494: "authorized principals");
! 495: }
1.1 djm 496: