1 /* $NetBSD: hostfile.c,v 1.23 2023/07/26 17:58:15 christos Exp $ */
2 /* $OpenBSD: hostfile.c,v 1.95 2023/02/21 06:48:18 dtucker Exp $ */
3 /*
4 * Author: Tatu Ylonen <ylo@cs.hut.fi>
5 * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
6 * All rights reserved
7 * Functions for manipulating the known hosts files.
8 *
9 * As far as I am concerned, the code I have written for this software
10 * can be used freely for any purpose. Any derived versions of this
11 * software must be clearly marked as such, and if the derived work is
12 * incompatible with the protocol description in the RFC file, it must be
13 * called by a name other than "ssh" or "Secure Shell".
14 *
15 *
16 * Copyright (c) 1999, 2000 Markus Friedl. All rights reserved.
17 * Copyright (c) 1999 Niels Provos. All rights reserved.
18 *
19 * Redistribution and use in source and binary forms, with or without
20 * modification, are permitted provided that the following conditions
21 * are met:
22 * 1. Redistributions of source code must retain the above copyright
23 * notice, this list of conditions and the following disclaimer.
24 * 2. Redistributions in binary form must reproduce the above copyright
25 * notice, this list of conditions and the following disclaimer in the
26 * documentation and/or other materials provided with the distribution.
27 *
28 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
29 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
30 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
31 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
32 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
33 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
37 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 */
39
40 #include "includes.h"
41 __RCSID("$NetBSD: hostfile.c,v 1.23 2023/07/26 17:58:15 christos Exp $");
42 #include <sys/types.h>
43 #include <sys/stat.h>
44
45 #include <netinet/in.h>
46
47 #include <errno.h>
48 #include <resolv.h>
49 #include <stdarg.h>
50 #include <stdio.h>
51 #include <stdlib.h>
52 #include <string.h>
53 #include <unistd.h>
54
55 #include "xmalloc.h"
56 #include "match.h"
57 #include "sshkey.h"
58 #include "hostfile.h"
59 #include "log.h"
60 #include "misc.h"
61 #include "pathnames.h"
62 #include "ssherr.h"
63 #include "digest.h"
64 #include "hmac.h"
65 #include "sshbuf.h"
66
67 /* XXX hmac is too easy to dictionary attack; use bcrypt? */
68
69 static int
extract_salt(const char * s,u_int l,u_char * salt,size_t salt_len)70 extract_salt(const char *s, u_int l, u_char *salt, size_t salt_len)
71 {
72 char *p, *b64salt;
73 u_int b64len;
74 int ret;
75
76 if (l < sizeof(HASH_MAGIC) - 1) {
77 debug2("extract_salt: string too short");
78 return (-1);
79 }
80 if (strncmp(s, HASH_MAGIC, sizeof(HASH_MAGIC) - 1) != 0) {
81 debug2("extract_salt: invalid magic identifier");
82 return (-1);
83 }
84 s += sizeof(HASH_MAGIC) - 1;
85 l -= sizeof(HASH_MAGIC) - 1;
86 if ((p = memchr(s, HASH_DELIM, l)) == NULL) {
87 debug2("extract_salt: missing salt termination character");
88 return (-1);
89 }
90
91 b64len = p - s;
92 /* Sanity check */
93 if (b64len == 0 || b64len > 1024) {
94 debug2("extract_salt: bad encoded salt length %u", b64len);
95 return (-1);
96 }
97 b64salt = xmalloc(1 + b64len);
98 memcpy(b64salt, s, b64len);
99 b64salt[b64len] = '\0';
100
101 ret = __b64_pton(b64salt, salt, salt_len);
102 free(b64salt);
103 if (ret == -1) {
104 debug2("extract_salt: salt decode error");
105 return (-1);
106 }
107 if (ret != (int)ssh_hmac_bytes(SSH_DIGEST_SHA1)) {
108 debug2("extract_salt: expected salt len %zd, got %d",
109 ssh_hmac_bytes(SSH_DIGEST_SHA1), ret);
110 return (-1);
111 }
112
113 return (0);
114 }
115
116 char *
host_hash(const char * host,const char * name_from_hostfile,u_int src_len)117 host_hash(const char *host, const char *name_from_hostfile, u_int src_len)
118 {
119 struct ssh_hmac_ctx *ctx;
120 u_char salt[256], result[256];
121 char uu_salt[512], uu_result[512];
122 char *encoded = NULL;
123 u_int len;
124
125 len = ssh_digest_bytes(SSH_DIGEST_SHA1);
126
127 if (name_from_hostfile == NULL) {
128 /* Create new salt */
129 arc4random_buf(salt, len);
130 } else {
131 /* Extract salt from known host entry */
132 if (extract_salt(name_from_hostfile, src_len, salt,
133 sizeof(salt)) == -1)
134 return (NULL);
135 }
136
137 if ((ctx = ssh_hmac_start(SSH_DIGEST_SHA1)) == NULL ||
138 ssh_hmac_init(ctx, salt, len) < 0 ||
139 ssh_hmac_update(ctx, host, strlen(host)) < 0 ||
140 ssh_hmac_final(ctx, result, sizeof(result)))
141 fatal_f("ssh_hmac failed");
142 ssh_hmac_free(ctx);
143
144 if (__b64_ntop(salt, len, uu_salt, sizeof(uu_salt)) == -1 ||
145 __b64_ntop(result, len, uu_result, sizeof(uu_result)) == -1)
146 fatal_f("__b64_ntop failed");
147 xasprintf(&encoded, "%s%s%c%s", HASH_MAGIC, uu_salt, HASH_DELIM,
148 uu_result);
149
150 return (encoded);
151 }
152
153 /*
154 * Parses an RSA (number of bits, e, n) or DSA key from a string. Moves the
155 * pointer over the key. Skips any whitespace at the beginning and at end.
156 */
157
158 int
hostfile_read_key(char ** cpp,u_int * bitsp,struct sshkey * ret)159 hostfile_read_key(char **cpp, u_int *bitsp, struct sshkey *ret)
160 {
161 char *cp;
162
163 /* Skip leading whitespace. */
164 for (cp = *cpp; *cp == ' ' || *cp == '\t'; cp++)
165 ;
166
167 if (sshkey_read(ret, &cp) != 0)
168 return 0;
169
170 /* Skip trailing whitespace. */
171 for (; *cp == ' ' || *cp == '\t'; cp++)
172 ;
173
174 /* Return results. */
175 *cpp = cp;
176 if (bitsp != NULL)
177 *bitsp = sshkey_size(ret);
178 return 1;
179 }
180
181 static HostkeyMarker
check_markers(char ** cpp)182 check_markers(char **cpp)
183 {
184 char marker[32], *sp, *cp = *cpp;
185 int ret = MRK_NONE;
186
187 while (*cp == '@') {
188 /* Only one marker is allowed */
189 if (ret != MRK_NONE)
190 return MRK_ERROR;
191 /* Markers are terminated by whitespace */
192 if ((sp = strchr(cp, ' ')) == NULL &&
193 (sp = strchr(cp, '\t')) == NULL)
194 return MRK_ERROR;
195 /* Extract marker for comparison */
196 if (sp <= cp + 1 || sp >= cp + sizeof(marker))
197 return MRK_ERROR;
198 memcpy(marker, cp, sp - cp);
199 marker[sp - cp] = '\0';
200 if (strcmp(marker, CA_MARKER) == 0)
201 ret = MRK_CA;
202 else if (strcmp(marker, REVOKE_MARKER) == 0)
203 ret = MRK_REVOKE;
204 else
205 return MRK_ERROR;
206
207 /* Skip past marker and any whitespace that follows it */
208 cp = sp;
209 for (; *cp == ' ' || *cp == '\t'; cp++)
210 ;
211 }
212 *cpp = cp;
213 return ret;
214 }
215
216 struct hostkeys *
init_hostkeys(void)217 init_hostkeys(void)
218 {
219 struct hostkeys *ret = xcalloc(1, sizeof(*ret));
220
221 ret->entries = NULL;
222 return ret;
223 }
224
225 struct load_callback_ctx {
226 const char *host;
227 u_long num_loaded;
228 struct hostkeys *hostkeys;
229 };
230
231 static int
record_hostkey(struct hostkey_foreach_line * l,void * _ctx)232 record_hostkey(struct hostkey_foreach_line *l, void *_ctx)
233 {
234 struct load_callback_ctx *ctx = (struct load_callback_ctx *)_ctx;
235 struct hostkeys *hostkeys = ctx->hostkeys;
236 struct hostkey_entry *tmp;
237
238 if (l->status == HKF_STATUS_INVALID) {
239 /* XXX make this verbose() in the future */
240 debug("%s:%ld: parse error in hostkeys file",
241 l->path, l->linenum);
242 return 0;
243 }
244
245 debug3_f("found %skey type %s in file %s:%lu",
246 l->marker == MRK_NONE ? "" :
247 (l->marker == MRK_CA ? "ca " : "revoked "),
248 sshkey_type(l->key), l->path, l->linenum);
249 if ((tmp = recallocarray(hostkeys->entries, hostkeys->num_entries,
250 hostkeys->num_entries + 1, sizeof(*hostkeys->entries))) == NULL)
251 return SSH_ERR_ALLOC_FAIL;
252 hostkeys->entries = tmp;
253 hostkeys->entries[hostkeys->num_entries].host = xstrdup(ctx->host);
254 hostkeys->entries[hostkeys->num_entries].file = xstrdup(l->path);
255 hostkeys->entries[hostkeys->num_entries].line = l->linenum;
256 hostkeys->entries[hostkeys->num_entries].key = l->key;
257 l->key = NULL; /* steal it */
258 hostkeys->entries[hostkeys->num_entries].marker = l->marker;
259 hostkeys->entries[hostkeys->num_entries].note = l->note;
260 hostkeys->num_entries++;
261 ctx->num_loaded++;
262
263 return 0;
264 }
265
266 void
load_hostkeys_file(struct hostkeys * hostkeys,const char * host,const char * path,FILE * f,u_int note)267 load_hostkeys_file(struct hostkeys *hostkeys, const char *host,
268 const char *path, FILE *f, u_int note)
269 {
270 int r;
271 struct load_callback_ctx ctx;
272
273 ctx.host = host;
274 ctx.num_loaded = 0;
275 ctx.hostkeys = hostkeys;
276
277 if ((r = hostkeys_foreach_file(path, f, record_hostkey, &ctx, host,
278 NULL, HKF_WANT_MATCH|HKF_WANT_PARSE_KEY, note)) != 0) {
279 if (r != SSH_ERR_SYSTEM_ERROR && errno != ENOENT)
280 debug_fr(r, "hostkeys_foreach failed for %s", path);
281 }
282 if (ctx.num_loaded != 0)
283 debug3_f("loaded %lu keys from %s", ctx.num_loaded, host);
284 }
285
286 void
load_hostkeys(struct hostkeys * hostkeys,const char * host,const char * path,u_int note)287 load_hostkeys(struct hostkeys *hostkeys, const char *host, const char *path,
288 u_int note)
289 {
290 FILE *f;
291
292 if ((f = fopen(path, "r")) == NULL) {
293 debug_f("fopen %s: %s", path, strerror(errno));
294 return;
295 }
296
297 load_hostkeys_file(hostkeys, host, path, f, note);
298 fclose(f);
299 }
300
301 void
free_hostkeys(struct hostkeys * hostkeys)302 free_hostkeys(struct hostkeys *hostkeys)
303 {
304 u_int i;
305
306 for (i = 0; i < hostkeys->num_entries; i++) {
307 free(hostkeys->entries[i].host);
308 free(hostkeys->entries[i].file);
309 sshkey_free(hostkeys->entries[i].key);
310 explicit_bzero(hostkeys->entries + i, sizeof(*hostkeys->entries));
311 }
312 free(hostkeys->entries);
313 freezero(hostkeys, sizeof(*hostkeys));
314 }
315
316 static int
check_key_not_revoked(struct hostkeys * hostkeys,struct sshkey * k)317 check_key_not_revoked(struct hostkeys *hostkeys, struct sshkey *k)
318 {
319 int is_cert = sshkey_is_cert(k);
320 u_int i;
321
322 for (i = 0; i < hostkeys->num_entries; i++) {
323 if (hostkeys->entries[i].marker != MRK_REVOKE)
324 continue;
325 if (sshkey_equal_public(k, hostkeys->entries[i].key))
326 return -1;
327 if (is_cert && k != NULL &&
328 sshkey_equal_public(k->cert->signature_key,
329 hostkeys->entries[i].key))
330 return -1;
331 }
332 return 0;
333 }
334
335 /*
336 * Match keys against a specified key, or look one up by key type.
337 *
338 * If looking for a keytype (key == NULL) and one is found then return
339 * HOST_FOUND, otherwise HOST_NEW.
340 *
341 * If looking for a key (key != NULL):
342 * 1. If the key is a cert and a matching CA is found, return HOST_OK
343 * 2. If the key is not a cert and a matching key is found, return HOST_OK
344 * 3. If no key matches but a key with a different type is found, then
345 * return HOST_CHANGED
346 * 4. If no matching keys are found, then return HOST_NEW.
347 *
348 * Finally, check any found key is not revoked.
349 */
350 static HostStatus
check_hostkeys_by_key_or_type(struct hostkeys * hostkeys,struct sshkey * k,int keytype,int nid,const struct hostkey_entry ** found)351 check_hostkeys_by_key_or_type(struct hostkeys *hostkeys,
352 struct sshkey *k, int keytype, int nid, const struct hostkey_entry **found)
353 {
354 u_int i;
355 HostStatus end_return = HOST_NEW;
356 int want_cert = sshkey_is_cert(k);
357 HostkeyMarker want_marker = want_cert ? MRK_CA : MRK_NONE;
358
359 if (found != NULL)
360 *found = NULL;
361
362 for (i = 0; i < hostkeys->num_entries; i++) {
363 if (hostkeys->entries[i].marker != want_marker)
364 continue;
365 if (k == NULL) {
366 if (hostkeys->entries[i].key->type != keytype)
367 continue;
368 if (nid != -1 &&
369 sshkey_type_plain(keytype) == KEY_ECDSA &&
370 hostkeys->entries[i].key->ecdsa_nid != nid)
371 continue;
372 end_return = HOST_FOUND;
373 if (found != NULL)
374 *found = hostkeys->entries + i;
375 k = hostkeys->entries[i].key;
376 break;
377 }
378 if (want_cert) {
379 if (sshkey_equal_public(k->cert->signature_key,
380 hostkeys->entries[i].key)) {
381 /* A matching CA exists */
382 end_return = HOST_OK;
383 if (found != NULL)
384 *found = hostkeys->entries + i;
385 break;
386 }
387 } else {
388 if (sshkey_equal(k, hostkeys->entries[i].key)) {
389 end_return = HOST_OK;
390 if (found != NULL)
391 *found = hostkeys->entries + i;
392 break;
393 }
394 /* A non-matching key exists */
395 end_return = HOST_CHANGED;
396 if (found != NULL)
397 *found = hostkeys->entries + i;
398 }
399 }
400 if (check_key_not_revoked(hostkeys, k) != 0) {
401 end_return = HOST_REVOKED;
402 if (found != NULL)
403 *found = NULL;
404 }
405 return end_return;
406 }
407
408 HostStatus
check_key_in_hostkeys(struct hostkeys * hostkeys,struct sshkey * key,const struct hostkey_entry ** found)409 check_key_in_hostkeys(struct hostkeys *hostkeys, struct sshkey *key,
410 const struct hostkey_entry **found)
411 {
412 if (key == NULL)
413 fatal("no key to look up");
414 return check_hostkeys_by_key_or_type(hostkeys, key, 0, -1, found);
415 }
416
417 int
lookup_key_in_hostkeys_by_type(struct hostkeys * hostkeys,int keytype,int nid,const struct hostkey_entry ** found)418 lookup_key_in_hostkeys_by_type(struct hostkeys *hostkeys, int keytype, int nid,
419 const struct hostkey_entry **found)
420 {
421 return (check_hostkeys_by_key_or_type(hostkeys, NULL, keytype, nid,
422 found) == HOST_FOUND);
423 }
424
425 int
lookup_marker_in_hostkeys(struct hostkeys * hostkeys,int want_marker)426 lookup_marker_in_hostkeys(struct hostkeys *hostkeys, int want_marker)
427 {
428 u_int i;
429
430 for (i = 0; i < hostkeys->num_entries; i++) {
431 if (hostkeys->entries[i].marker == (HostkeyMarker)want_marker)
432 return 1;
433 }
434 return 0;
435 }
436
437 static int
write_host_entry(FILE * f,const char * host,const char * ip,const struct sshkey * key,int store_hash)438 write_host_entry(FILE *f, const char *host, const char *ip,
439 const struct sshkey *key, int store_hash)
440 {
441 int r, success = 0;
442 char *hashed_host = NULL, *lhost;
443
444 lhost = xstrdup(host);
445 lowercase(lhost);
446
447 if (store_hash) {
448 if ((hashed_host = host_hash(lhost, NULL, 0)) == NULL) {
449 error_f("host_hash failed");
450 free(lhost);
451 return 0;
452 }
453 fprintf(f, "%s ", hashed_host);
454 } else if (ip != NULL)
455 fprintf(f, "%s,%s ", lhost, ip);
456 else {
457 fprintf(f, "%s ", lhost);
458 }
459 free(hashed_host);
460 free(lhost);
461 if ((r = sshkey_write(key, f)) == 0)
462 success = 1;
463 else
464 error_fr(r, "sshkey_write");
465 fputc('\n', f);
466 /* If hashing is enabled, the IP address needs to go on its own line */
467 if (success && store_hash && ip != NULL)
468 success = write_host_entry(f, ip, NULL, key, 1);
469 return success;
470 }
471
472 /*
473 * Create user ~/.ssh directory if it doesn't exist and we want to write to it.
474 * If notify is set, a message will be emitted if the directory is created.
475 */
476 void
hostfile_create_user_ssh_dir(const char * filename,int notify)477 hostfile_create_user_ssh_dir(const char *filename, int notify)
478 {
479 char *dotsshdir = NULL, *p;
480 size_t len;
481 struct stat st;
482
483 if ((p = strrchr(filename, '/')) == NULL)
484 return;
485 len = p - filename;
486 dotsshdir = tilde_expand_filename("~/" _PATH_SSH_USER_DIR, getuid());
487 if (strlen(dotsshdir) > len || strncmp(filename, dotsshdir, len) != 0)
488 goto out; /* not ~/.ssh prefixed */
489 if (stat(dotsshdir, &st) == 0)
490 goto out; /* dir already exists */
491 else if (errno != ENOENT)
492 error("Could not stat %s: %s", dotsshdir, strerror(errno));
493 else {
494 if (mkdir(dotsshdir, 0700) == -1)
495 error("Could not create directory '%.200s' (%s).",
496 dotsshdir, strerror(errno));
497 else if (notify)
498 logit("Created directory '%s'.", dotsshdir);
499 }
500 out:
501 free(dotsshdir);
502 }
503
504
505 /*
506 * Appends an entry to the host file. Returns false if the entry could not
507 * be appended.
508 */
509 int
add_host_to_hostfile(const char * filename,const char * host,const struct sshkey * key,int store_hash)510 add_host_to_hostfile(const char *filename, const char *host,
511 const struct sshkey *key, int store_hash)
512 {
513 FILE *f;
514 int success, addnl = 0;
515
516 if (key == NULL)
517 return 1; /* XXX ? */
518 hostfile_create_user_ssh_dir(filename, 0);
519 f = fopen(filename, "a+");
520 if (!f)
521 return 0;
522 /* Make sure we have a terminating newline. */
523 if (fseek(f, -1L, SEEK_END) == 0 && fgetc(f) != '\n')
524 addnl = 1;
525 if (fseek(f, 0L, SEEK_END) != 0 || (addnl && fputc('\n', f) != '\n')) {
526 error("Failed to add terminating newline to %s: %s",
527 filename, strerror(errno));
528 fclose(f);
529 return 0;
530 }
531 success = write_host_entry(f, host, NULL, key, store_hash);
532 fclose(f);
533 return success;
534 }
535
536 struct host_delete_ctx {
537 FILE *out;
538 int quiet;
539 const char *host, *ip;
540 u_int *match_keys; /* mask of HKF_MATCH_* for this key */
541 struct sshkey * const *keys;
542 size_t nkeys;
543 int modified;
544 };
545
546 static int
host_delete(struct hostkey_foreach_line * l,void * _ctx)547 host_delete(struct hostkey_foreach_line *l, void *_ctx)
548 {
549 struct host_delete_ctx *ctx = (struct host_delete_ctx *)_ctx;
550 int loglevel = ctx->quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_VERBOSE;
551 size_t i;
552
553 /* Don't remove CA and revocation lines */
554 if (l->status == HKF_STATUS_MATCHED && l->marker == MRK_NONE) {
555 /*
556 * If this line contains one of the keys that we will be
557 * adding later, then don't change it and mark the key for
558 * skipping.
559 */
560 for (i = 0; i < ctx->nkeys; i++) {
561 if (!sshkey_equal(ctx->keys[i], l->key))
562 continue;
563 ctx->match_keys[i] |= l->match;
564 fprintf(ctx->out, "%s\n", l->line);
565 debug3_f("%s key already at %s:%ld",
566 sshkey_type(l->key), l->path, l->linenum);
567 return 0;
568 }
569
570 /*
571 * Hostname matches and has no CA/revoke marker, delete it
572 * by *not* writing the line to ctx->out.
573 */
574 do_log2(loglevel, "%s%s%s:%ld: Removed %s key for host %s",
575 ctx->quiet ? __func__ : "", ctx->quiet ? ": " : "",
576 l->path, l->linenum, sshkey_type(l->key), ctx->host);
577 ctx->modified = 1;
578 return 0;
579 }
580 /* Retain non-matching hosts and invalid lines when deleting */
581 if (l->status == HKF_STATUS_INVALID) {
582 do_log2(loglevel, "%s%s%s:%ld: invalid known_hosts entry",
583 ctx->quiet ? __func__ : "", ctx->quiet ? ": " : "",
584 l->path, l->linenum);
585 }
586 fprintf(ctx->out, "%s\n", l->line);
587 return 0;
588 }
589
590 int
hostfile_replace_entries(const char * filename,const char * host,const char * ip,struct sshkey ** keys,size_t nkeys,int store_hash,int quiet,int hash_alg)591 hostfile_replace_entries(const char *filename, const char *host, const char *ip,
592 struct sshkey **keys, size_t nkeys, int store_hash, int quiet, int hash_alg)
593 {
594 int r, fd, oerrno = 0;
595 int loglevel = quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_VERBOSE;
596 struct host_delete_ctx ctx;
597 char *fp, *temp = NULL, *back = NULL;
598 const char *what;
599 mode_t omask;
600 size_t i;
601 u_int want;
602
603 omask = umask(077);
604
605 memset(&ctx, 0, sizeof(ctx));
606 ctx.host = host;
607 ctx.ip = ip;
608 ctx.quiet = quiet;
609
610 if ((ctx.match_keys = calloc(nkeys, sizeof(*ctx.match_keys))) == NULL)
611 return SSH_ERR_ALLOC_FAIL;
612 ctx.keys = keys;
613 ctx.nkeys = nkeys;
614 ctx.modified = 0;
615
616 /*
617 * Prepare temporary file for in-place deletion.
618 */
619 if ((r = asprintf(&temp, "%s.XXXXXXXXXXX", filename)) == -1 ||
620 (r = asprintf(&back, "%s.old", filename)) == -1) {
621 r = SSH_ERR_ALLOC_FAIL;
622 goto fail;
623 }
624
625 if ((fd = mkstemp(temp)) == -1) {
626 oerrno = errno;
627 error_f("mkstemp: %s", strerror(oerrno));
628 r = SSH_ERR_SYSTEM_ERROR;
629 goto fail;
630 }
631 if ((ctx.out = fdopen(fd, "w")) == NULL) {
632 oerrno = errno;
633 close(fd);
634 error_f("fdopen: %s", strerror(oerrno));
635 r = SSH_ERR_SYSTEM_ERROR;
636 goto fail;
637 }
638
639 /* Remove stale/mismatching entries for the specified host */
640 if ((r = hostkeys_foreach(filename, host_delete, &ctx, host, ip,
641 HKF_WANT_PARSE_KEY, 0)) != 0) {
642 oerrno = errno;
643 error_fr(r, "hostkeys_foreach");
644 goto fail;
645 }
646
647 /* Re-add the requested keys */
648 want = HKF_MATCH_HOST | (ip == NULL ? 0 : HKF_MATCH_IP);
649 for (i = 0; i < nkeys; i++) {
650 if (keys[i] == NULL || (want & ctx.match_keys[i]) == want)
651 continue;
652 if ((fp = sshkey_fingerprint(keys[i], hash_alg,
653 SSH_FP_DEFAULT)) == NULL) {
654 r = SSH_ERR_ALLOC_FAIL;
655 goto fail;
656 }
657 /* write host/ip */
658 what = "";
659 if (ctx.match_keys[i] == 0) {
660 what = "Adding new key";
661 if (!write_host_entry(ctx.out, host, ip,
662 keys[i], store_hash)) {
663 r = SSH_ERR_INTERNAL_ERROR;
664 goto fail;
665 }
666 } else if ((want & ~ctx.match_keys[i]) == HKF_MATCH_HOST) {
667 what = "Fixing match (hostname)";
668 if (!write_host_entry(ctx.out, host, NULL,
669 keys[i], store_hash)) {
670 r = SSH_ERR_INTERNAL_ERROR;
671 goto fail;
672 }
673 } else if ((want & ~ctx.match_keys[i]) == HKF_MATCH_IP) {
674 what = "Fixing match (address)";
675 if (!write_host_entry(ctx.out, ip, NULL,
676 keys[i], store_hash)) {
677 r = SSH_ERR_INTERNAL_ERROR;
678 goto fail;
679 }
680 }
681 do_log2(loglevel, "%s%s%s for %s%s%s to %s: %s %s",
682 quiet ? __func__ : "", quiet ? ": " : "", what,
683 host, ip == NULL ? "" : ",", ip == NULL ? "" : ip, filename,
684 sshkey_ssh_name(keys[i]), fp);
685 free(fp);
686 ctx.modified = 1;
687 }
688 fclose(ctx.out);
689 ctx.out = NULL;
690
691 if (ctx.modified) {
692 /* Backup the original file and replace it with the temporary */
693 if (unlink(back) == -1 && errno != ENOENT) {
694 oerrno = errno;
695 error_f("unlink %.100s: %s", back, strerror(errno));
696 r = SSH_ERR_SYSTEM_ERROR;
697 goto fail;
698 }
699 if (link(filename, back) == -1) {
700 oerrno = errno;
701 error_f("link %.100s to %.100s: %s", filename,
702 back, strerror(errno));
703 r = SSH_ERR_SYSTEM_ERROR;
704 goto fail;
705 }
706 if (rename(temp, filename) == -1) {
707 oerrno = errno;
708 error_f("rename \"%s\" to \"%s\": %s", temp,
709 filename, strerror(errno));
710 r = SSH_ERR_SYSTEM_ERROR;
711 goto fail;
712 }
713 } else {
714 /* No changes made; just delete the temporary file */
715 if (unlink(temp) != 0)
716 error_f("unlink \"%s\": %s", temp, strerror(errno));
717 }
718
719 /* success */
720 r = 0;
721 fail:
722 if (temp != NULL && r != 0)
723 unlink(temp);
724 free(temp);
725 free(back);
726 if (ctx.out != NULL)
727 fclose(ctx.out);
728 free(ctx.match_keys);
729 umask(omask);
730 if (r == SSH_ERR_SYSTEM_ERROR)
731 errno = oerrno;
732 return r;
733 }
734
735 static int
match_maybe_hashed(const char * host,const char * names,int * was_hashed)736 match_maybe_hashed(const char *host, const char *names, int *was_hashed)
737 {
738 int hashed = *names == HASH_DELIM, ret;
739 char *hashed_host = NULL;
740 size_t nlen = strlen(names);
741
742 if (was_hashed != NULL)
743 *was_hashed = hashed;
744 if (hashed) {
745 if ((hashed_host = host_hash(host, names, nlen)) == NULL)
746 return -1;
747 ret = (nlen == strlen(hashed_host) &&
748 strncmp(hashed_host, names, nlen) == 0);
749 free(hashed_host);
750 return ret;
751 }
752 return match_hostname(host, names) == 1;
753 }
754
755 int
hostkeys_foreach_file(const char * path,FILE * f,hostkeys_foreach_fn * callback,void * ctx,const char * host,const char * ip,u_int options,u_int note)756 hostkeys_foreach_file(const char *path, FILE *f, hostkeys_foreach_fn *callback,
757 void *ctx, const char *host, const char *ip, u_int options, u_int note)
758 {
759 char *line = NULL, ktype[128];
760 u_long linenum = 0;
761 char *cp, *cp2;
762 u_int kbits;
763 int hashed;
764 int s, r = 0;
765 struct hostkey_foreach_line lineinfo;
766 size_t linesize = 0, l;
767
768 memset(&lineinfo, 0, sizeof(lineinfo));
769 if (host == NULL && (options & HKF_WANT_MATCH) != 0)
770 return SSH_ERR_INVALID_ARGUMENT;
771
772 while (getline(&line, &linesize, f) != -1) {
773 linenum++;
774 line[strcspn(line, "\n")] = '\0';
775
776 free(lineinfo.line);
777 sshkey_free(lineinfo.key);
778 memset(&lineinfo, 0, sizeof(lineinfo));
779 lineinfo.path = path;
780 lineinfo.linenum = linenum;
781 lineinfo.line = xstrdup(line);
782 lineinfo.marker = MRK_NONE;
783 lineinfo.status = HKF_STATUS_OK;
784 lineinfo.keytype = KEY_UNSPEC;
785 lineinfo.note = note;
786
787 /* Skip any leading whitespace, comments and empty lines. */
788 for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
789 ;
790 if (!*cp || *cp == '#' || *cp == '\n') {
791 if ((options & HKF_WANT_MATCH) == 0) {
792 lineinfo.status = HKF_STATUS_COMMENT;
793 if ((r = callback(&lineinfo, ctx)) != 0)
794 break;
795 }
796 continue;
797 }
798
799 if ((lineinfo.marker = check_markers(&cp)) == MRK_ERROR) {
800 verbose_f("invalid marker at %s:%lu", path, linenum);
801 if ((options & HKF_WANT_MATCH) == 0)
802 goto bad;
803 continue;
804 }
805
806 /* Find the end of the host name portion. */
807 for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t'; cp2++)
808 ;
809 lineinfo.hosts = cp;
810 *cp2++ = '\0';
811
812 /* Check if the host name matches. */
813 if (host != NULL) {
814 if ((s = match_maybe_hashed(host, lineinfo.hosts,
815 &hashed)) == -1) {
816 debug2_f("%s:%ld: bad host hash \"%.32s\"",
817 path, linenum, lineinfo.hosts);
818 goto bad;
819 }
820 if (s == 1) {
821 lineinfo.status = HKF_STATUS_MATCHED;
822 lineinfo.match |= HKF_MATCH_HOST |
823 (hashed ? HKF_MATCH_HOST_HASHED : 0);
824 }
825 /* Try matching IP address if supplied */
826 if (ip != NULL) {
827 if ((s = match_maybe_hashed(ip, lineinfo.hosts,
828 &hashed)) == -1) {
829 debug2_f("%s:%ld: bad ip hash "
830 "\"%.32s\"", path, linenum,
831 lineinfo.hosts);
832 goto bad;
833 }
834 if (s == 1) {
835 lineinfo.status = HKF_STATUS_MATCHED;
836 lineinfo.match |= HKF_MATCH_IP |
837 (hashed ? HKF_MATCH_IP_HASHED : 0);
838 }
839 }
840 /*
841 * Skip this line if host matching requested and
842 * neither host nor address matched.
843 */
844 if ((options & HKF_WANT_MATCH) != 0 &&
845 lineinfo.status != HKF_STATUS_MATCHED)
846 continue;
847 }
848
849 /* Got a match. Skip host name and any following whitespace */
850 for (; *cp2 == ' ' || *cp2 == '\t'; cp2++)
851 ;
852 if (*cp2 == '\0' || *cp2 == '#') {
853 debug2("%s:%ld: truncated before key type",
854 path, linenum);
855 goto bad;
856 }
857 lineinfo.rawkey = cp = cp2;
858
859 if ((options & HKF_WANT_PARSE_KEY) != 0) {
860 /*
861 * Extract the key from the line. This will skip
862 * any leading whitespace. Ignore badly formatted
863 * lines.
864 */
865 if ((lineinfo.key = sshkey_new(KEY_UNSPEC)) == NULL) {
866 error_f("sshkey_new failed");
867 r = SSH_ERR_ALLOC_FAIL;
868 break;
869 }
870 if (!hostfile_read_key(&cp, &kbits, lineinfo.key)) {
871 goto bad;
872 }
873 lineinfo.keytype = lineinfo.key->type;
874 lineinfo.comment = cp;
875 } else {
876 /* Extract and parse key type */
877 l = strcspn(lineinfo.rawkey, " \t");
878 if (l <= 1 || l >= sizeof(ktype) ||
879 lineinfo.rawkey[l] == '\0')
880 goto bad;
881 memcpy(ktype, lineinfo.rawkey, l);
882 ktype[l] = '\0';
883 lineinfo.keytype = sshkey_type_from_name(ktype);
884
885 /*
886 * Assume legacy RSA1 if the first component is a short
887 * decimal number.
888 */
889 if (lineinfo.keytype == KEY_UNSPEC && l < 8 &&
890 strspn(ktype, "0123456789") == l)
891 goto bad;
892
893 /*
894 * Check that something other than whitespace follows
895 * the key type. This won't catch all corruption, but
896 * it does catch trivial truncation.
897 */
898 cp2 += l; /* Skip past key type */
899 for (; *cp2 == ' ' || *cp2 == '\t'; cp2++)
900 ;
901 if (*cp2 == '\0' || *cp2 == '#') {
902 debug2("%s:%ld: truncated after key type",
903 path, linenum);
904 lineinfo.keytype = KEY_UNSPEC;
905 }
906 if (lineinfo.keytype == KEY_UNSPEC) {
907 bad:
908 sshkey_free(lineinfo.key);
909 lineinfo.key = NULL;
910 lineinfo.status = HKF_STATUS_INVALID;
911 if ((r = callback(&lineinfo, ctx)) != 0)
912 break;
913 continue;
914 }
915 }
916 if ((r = callback(&lineinfo, ctx)) != 0)
917 break;
918 }
919 sshkey_free(lineinfo.key);
920 free(lineinfo.line);
921 free(line);
922 return r;
923 }
924
925 int
hostkeys_foreach(const char * path,hostkeys_foreach_fn * callback,void * ctx,const char * host,const char * ip,u_int options,u_int note)926 hostkeys_foreach(const char *path, hostkeys_foreach_fn *callback, void *ctx,
927 const char *host, const char *ip, u_int options, u_int note)
928 {
929 FILE *f;
930 int r, oerrno;
931
932 if ((f = fopen(path, "r")) == NULL)
933 return SSH_ERR_SYSTEM_ERROR;
934
935 debug3_f("reading file \"%s\"", path);
936 r = hostkeys_foreach_file(path, f, callback, ctx, host, ip,
937 options, note);
938 oerrno = errno;
939 fclose(f);
940 errno = oerrno;
941 return r;
942 }
943