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