diff options
author | djm@openbsd.org <djm@openbsd.org> | 2015-02-16 22:13:32 +0000 |
---|---|---|
committer | Damien Miller <djm@mindrot.org> | 2015-02-17 09:32:32 +1100 |
commit | 523463a3a2a9bfc6cfc5afa01bae9147f76a37cc (patch) | |
tree | 772be92cee9553c19d51b4570113c3d4de0c2d8b /clientloop.c | |
parent | 6c5c949782d86a6e7d58006599c7685bfcd01685 (diff) |
upstream commit
Revise hostkeys@openssh.com hostkey learning extension.
The client will not ask the server to prove ownership of the private
halves of any hitherto-unseen hostkeys it offers to the client.
Allow UpdateHostKeys option to take an 'ask' argument to let the
user manually review keys offered.
ok markus@
Diffstat (limited to 'clientloop.c')
-rw-r--r-- | clientloop.c | 353 |
1 files changed, 312 insertions, 41 deletions
diff --git a/clientloop.c b/clientloop.c index c6f8e9dc1..a19d9d06f 100644 --- a/clientloop.c +++ b/clientloop.c | |||
@@ -1,4 +1,4 @@ | |||
1 | /* $OpenBSD: clientloop.c,v 1.268 2015/02/16 22:08:57 djm Exp $ */ | 1 | /* $OpenBSD: clientloop.c,v 1.269 2015/02/16 22:13:32 djm Exp $ */ |
2 | /* | 2 | /* |
3 | * Author: Tatu Ylonen <ylo@cs.hut.fi> | 3 | * Author: Tatu Ylonen <ylo@cs.hut.fi> |
4 | * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland | 4 | * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland |
@@ -2089,6 +2089,216 @@ client_input_channel_req(int type, u_int32_t seq, void *ctxt) | |||
2089 | return 0; | 2089 | return 0; |
2090 | } | 2090 | } |
2091 | 2091 | ||
2092 | struct hostkeys_update_ctx { | ||
2093 | /* The hostname and (optionally) IP address string for the server */ | ||
2094 | char *host_str, *ip_str; | ||
2095 | |||
2096 | /* | ||
2097 | * Keys received from the server and a flag for each indicating | ||
2098 | * whether they already exist in known_hosts. | ||
2099 | * keys_seen is filled in by hostkeys_find() and later (for new | ||
2100 | * keys) by client_global_hostkeys_private_confirm(). | ||
2101 | */ | ||
2102 | struct sshkey **keys; | ||
2103 | int *keys_seen; | ||
2104 | size_t nkeys; | ||
2105 | |||
2106 | size_t nnew; | ||
2107 | |||
2108 | /* | ||
2109 | * Keys that are in known_hosts, but were not present in the update | ||
2110 | * from the server (i.e. scheduled to be deleted). | ||
2111 | * Filled in by hostkeys_find(). | ||
2112 | */ | ||
2113 | struct sshkey **old_keys; | ||
2114 | size_t nold; | ||
2115 | }; | ||
2116 | |||
2117 | static void | ||
2118 | hostkeys_update_ctx_free(struct hostkeys_update_ctx *ctx) | ||
2119 | { | ||
2120 | size_t i; | ||
2121 | |||
2122 | if (ctx == NULL) | ||
2123 | return; | ||
2124 | for (i = 0; i < ctx->nkeys; i++) | ||
2125 | sshkey_free(ctx->keys[i]); | ||
2126 | free(ctx->keys); | ||
2127 | free(ctx->keys_seen); | ||
2128 | for (i = 0; i < ctx->nold; i++) | ||
2129 | sshkey_free(ctx->old_keys[i]); | ||
2130 | free(ctx->old_keys); | ||
2131 | free(ctx->host_str); | ||
2132 | free(ctx->ip_str); | ||
2133 | free(ctx); | ||
2134 | } | ||
2135 | |||
2136 | static int | ||
2137 | hostkeys_find(struct hostkey_foreach_line *l, void *_ctx) | ||
2138 | { | ||
2139 | struct hostkeys_update_ctx *ctx = (struct hostkeys_update_ctx *)_ctx; | ||
2140 | size_t i; | ||
2141 | struct sshkey **tmp; | ||
2142 | |||
2143 | if (l->status != HKF_STATUS_MATCHED || l->key == NULL || | ||
2144 | l->key->type == KEY_RSA1) | ||
2145 | return 0; | ||
2146 | |||
2147 | /* Mark off keys we've already seen for this host */ | ||
2148 | for (i = 0; i < ctx->nkeys; i++) { | ||
2149 | if (sshkey_equal(l->key, ctx->keys[i])) { | ||
2150 | debug3("%s: found %s key at %s:%ld", __func__, | ||
2151 | sshkey_ssh_name(ctx->keys[i]), l->path, l->linenum); | ||
2152 | ctx->keys_seen[i] = 1; | ||
2153 | return 0; | ||
2154 | } | ||
2155 | } | ||
2156 | /* This line contained a key that not offered by the server */ | ||
2157 | debug3("%s: deprecated %s key at %s:%ld", __func__, | ||
2158 | sshkey_ssh_name(l->key), l->path, l->linenum); | ||
2159 | if ((tmp = reallocarray(ctx->old_keys, ctx->nold + 1, | ||
2160 | sizeof(*ctx->old_keys))) == NULL) | ||
2161 | fatal("%s: reallocarray failed nold = %zu", | ||
2162 | __func__, ctx->nold); | ||
2163 | ctx->old_keys = tmp; | ||
2164 | ctx->old_keys[ctx->nold++] = l->key; | ||
2165 | l->key = NULL; | ||
2166 | |||
2167 | return 0; | ||
2168 | } | ||
2169 | |||
2170 | static void | ||
2171 | update_known_hosts(struct hostkeys_update_ctx *ctx) | ||
2172 | { | ||
2173 | int r, loglevel = options.update_hostkeys == SSH_UPDATE_HOSTKEYS_ASK ? | ||
2174 | SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_VERBOSE; | ||
2175 | char *fp, *response; | ||
2176 | size_t i; | ||
2177 | |||
2178 | for (i = 0; i < ctx->nkeys; i++) { | ||
2179 | if (ctx->keys_seen[i] != 2) | ||
2180 | continue; | ||
2181 | if ((fp = sshkey_fingerprint(ctx->keys[i], | ||
2182 | options.fingerprint_hash, SSH_FP_DEFAULT)) == NULL) | ||
2183 | fatal("%s: sshkey_fingerprint failed", __func__); | ||
2184 | do_log2(loglevel, "Learned new hostkey: %s %s", | ||
2185 | sshkey_type(ctx->keys[i]), fp); | ||
2186 | free(fp); | ||
2187 | } | ||
2188 | for (i = 0; i < ctx->nold; i++) { | ||
2189 | if ((fp = sshkey_fingerprint(ctx->old_keys[i], | ||
2190 | options.fingerprint_hash, SSH_FP_DEFAULT)) == NULL) | ||
2191 | fatal("%s: sshkey_fingerprint failed", __func__); | ||
2192 | do_log2(loglevel, "Deprecating obsolete hostkey: %s %s", | ||
2193 | sshkey_type(ctx->old_keys[i]), fp); | ||
2194 | free(fp); | ||
2195 | } | ||
2196 | if (options.update_hostkeys == SSH_UPDATE_HOSTKEYS_ASK) { | ||
2197 | leave_raw_mode(options.request_tty == REQUEST_TTY_FORCE); | ||
2198 | response = NULL; | ||
2199 | for (i = 0; !quit_pending && i < 3; i++) { | ||
2200 | free(response); | ||
2201 | response = read_passphrase("Accept updated hostkeys? " | ||
2202 | "(yes/no): ", RP_ECHO); | ||
2203 | if (strcasecmp(response, "yes") == 0) | ||
2204 | break; | ||
2205 | else if (quit_pending || response == NULL || | ||
2206 | strcasecmp(response, "no") == 0) { | ||
2207 | options.update_hostkeys = 0; | ||
2208 | break; | ||
2209 | } else { | ||
2210 | do_log2(loglevel, "Please enter " | ||
2211 | "\"yes\" or \"no\""); | ||
2212 | } | ||
2213 | } | ||
2214 | if (quit_pending || i >= 3 || response == NULL) | ||
2215 | options.update_hostkeys = 0; | ||
2216 | free(response); | ||
2217 | enter_raw_mode(options.request_tty == REQUEST_TTY_FORCE); | ||
2218 | } | ||
2219 | |||
2220 | /* | ||
2221 | * Now that all the keys are verified, we can go ahead and replace | ||
2222 | * them in known_hosts (assuming SSH_UPDATE_HOSTKEYS_ASK didn't | ||
2223 | * cancel the operation). | ||
2224 | */ | ||
2225 | if (options.update_hostkeys != 0 && | ||
2226 | (r = hostfile_replace_entries(options.user_hostfiles[0], | ||
2227 | ctx->host_str, ctx->ip_str, ctx->keys, ctx->nkeys, | ||
2228 | options.hash_known_hosts, 0, | ||
2229 | options.fingerprint_hash)) != 0) | ||
2230 | error("%s: hostfile_replace_entries failed: %s", | ||
2231 | __func__, ssh_err(r)); | ||
2232 | } | ||
2233 | |||
2234 | static void | ||
2235 | client_global_hostkeys_private_confirm(int type, u_int32_t seq, void *_ctx) | ||
2236 | { | ||
2237 | struct ssh *ssh = active_state; /* XXX */ | ||
2238 | struct hostkeys_update_ctx *ctx = (struct hostkeys_update_ctx *)_ctx; | ||
2239 | size_t i, ndone; | ||
2240 | struct sshbuf *signdata; | ||
2241 | int r; | ||
2242 | const u_char *sig; | ||
2243 | size_t siglen; | ||
2244 | |||
2245 | if (ctx->nnew == 0) | ||
2246 | fatal("%s: ctx->nnew == 0", __func__); /* sanity */ | ||
2247 | if (type != SSH2_MSG_REQUEST_SUCCESS) { | ||
2248 | error("Server failed to confirm ownership of " | ||
2249 | "private host keys"); | ||
2250 | hostkeys_update_ctx_free(ctx); | ||
2251 | return; | ||
2252 | } | ||
2253 | if ((signdata = sshbuf_new()) == NULL) | ||
2254 | fatal("%s: sshbuf_new failed", __func__); | ||
2255 | /* Don't want to accidentally accept an unbound signature */ | ||
2256 | if (ssh->kex->session_id_len == 0) | ||
2257 | fatal("%s: ssh->kex->session_id_len == 0", __func__); | ||
2258 | /* | ||
2259 | * Expect a signature for each of the ctx->nnew private keys we | ||
2260 | * haven't seen before. They will be in the same order as the | ||
2261 | * ctx->keys where the corresponding ctx->keys_seen[i] == 0. | ||
2262 | */ | ||
2263 | for (ndone = i = 0; i < ctx->nkeys; i++) { | ||
2264 | if (ctx->keys_seen[i]) | ||
2265 | continue; | ||
2266 | /* Prepare data to be signed: session ID, unique string, key */ | ||
2267 | sshbuf_reset(signdata); | ||
2268 | if ((r = sshbuf_put_string(signdata, ssh->kex->session_id, | ||
2269 | ssh->kex->session_id_len)) != 0 || | ||
2270 | (r = sshbuf_put_cstring(signdata, | ||
2271 | "hostkeys-prove@openssh.com")) != 0 || | ||
2272 | (r = sshkey_puts(ctx->keys[i], signdata)) != 0) | ||
2273 | fatal("%s: failed to prepare signature: %s", | ||
2274 | __func__, ssh_err(r)); | ||
2275 | /* Extract and verify signature */ | ||
2276 | if ((r = sshpkt_get_string_direct(ssh, &sig, &siglen)) != 0) { | ||
2277 | error("%s: couldn't parse message: %s", | ||
2278 | __func__, ssh_err(r)); | ||
2279 | goto out; | ||
2280 | } | ||
2281 | if ((r = sshkey_verify(ctx->keys[i], sig, siglen, | ||
2282 | sshbuf_ptr(signdata), sshbuf_len(signdata), 0)) != 0) { | ||
2283 | error("%s: server gave bad signature for %s key %zu", | ||
2284 | __func__, sshkey_type(ctx->keys[i]), i); | ||
2285 | goto out; | ||
2286 | } | ||
2287 | /* Key is good. Mark it as 'seen' */ | ||
2288 | ctx->keys_seen[i] = 2; | ||
2289 | ndone++; | ||
2290 | } | ||
2291 | if (ndone != ctx->nnew) | ||
2292 | fatal("%s: ndone != ctx->nnew (%zu / %zu)", __func__, | ||
2293 | ndone, ctx->nnew); /* Shouldn't happen */ | ||
2294 | ssh_packet_check_eom(ssh); | ||
2295 | |||
2296 | /* Make the edits to known_hosts */ | ||
2297 | update_known_hosts(ctx); | ||
2298 | out: | ||
2299 | hostkeys_update_ctx_free(ctx); | ||
2300 | } | ||
2301 | |||
2092 | /* | 2302 | /* |
2093 | * Handle hostkeys@openssh.com global request to inform the client of all | 2303 | * Handle hostkeys@openssh.com global request to inform the client of all |
2094 | * the server's hostkeys. The keys are checked against the user's | 2304 | * the server's hostkeys. The keys are checked against the user's |
@@ -2097,34 +2307,35 @@ client_input_channel_req(int type, u_int32_t seq, void *ctxt) | |||
2097 | static int | 2307 | static int |
2098 | client_input_hostkeys(void) | 2308 | client_input_hostkeys(void) |
2099 | { | 2309 | { |
2310 | struct ssh *ssh = active_state; /* XXX */ | ||
2100 | const u_char *blob = NULL; | 2311 | const u_char *blob = NULL; |
2101 | u_int i, len = 0, nkeys = 0; | 2312 | size_t i, len = 0; |
2102 | struct sshbuf *buf = NULL; | 2313 | struct sshbuf *buf = NULL; |
2103 | struct sshkey *key = NULL, **tmp, **keys = NULL; | 2314 | struct sshkey *key = NULL, **tmp; |
2104 | int r, success = 1; | 2315 | int r; |
2105 | char *fp, *host_str = NULL, *ip_str = NULL; | 2316 | char *fp; |
2106 | static int hostkeys_seen = 0; /* XXX use struct ssh */ | 2317 | static int hostkeys_seen = 0; /* XXX use struct ssh */ |
2107 | extern struct sockaddr_storage hostaddr; /* XXX from ssh.c */ | 2318 | extern struct sockaddr_storage hostaddr; /* XXX from ssh.c */ |
2319 | struct hostkeys_update_ctx *ctx; | ||
2108 | 2320 | ||
2109 | /* | 2321 | ctx = xcalloc(1, sizeof(*ctx)); |
2110 | * NB. Return success for all cases other than protocol error. The | ||
2111 | * server doesn't need to know what the client does with its hosts | ||
2112 | * file. | ||
2113 | */ | ||
2114 | |||
2115 | blob = packet_get_string_ptr(&len); | ||
2116 | packet_check_eom(); | ||
2117 | 2322 | ||
2118 | if (hostkeys_seen) | 2323 | if (hostkeys_seen) |
2119 | fatal("%s: server already sent hostkeys", __func__); | 2324 | fatal("%s: server already sent hostkeys", __func__); |
2325 | if (options.update_hostkeys == SSH_UPDATE_HOSTKEYS_ASK && | ||
2326 | options.batch_mode) | ||
2327 | return 1; /* won't ask in batchmode, so don't even try */ | ||
2120 | if (!options.update_hostkeys || options.num_user_hostfiles <= 0) | 2328 | if (!options.update_hostkeys || options.num_user_hostfiles <= 0) |
2121 | return 1; | 2329 | return 1; |
2122 | if ((buf = sshbuf_from(blob, len)) == NULL) | 2330 | while (ssh_packet_remaining(ssh) > 0) { |
2123 | fatal("%s: sshbuf_from failed", __func__); | ||
2124 | while (sshbuf_len(buf) > 0) { | ||
2125 | sshkey_free(key); | 2331 | sshkey_free(key); |
2126 | key = NULL; | 2332 | key = NULL; |
2127 | if ((r = sshkey_froms(buf, &key)) != 0) | 2333 | if ((r = sshpkt_get_string_direct(ssh, &blob, &len)) != 0) { |
2334 | error("%s: couldn't parse message: %s", | ||
2335 | __func__, ssh_err(r)); | ||
2336 | goto out; | ||
2337 | } | ||
2338 | if ((r = sshkey_from_blob(blob, len, &key)) != 0) | ||
2128 | fatal("%s: parse key: %s", __func__, ssh_err(r)); | 2339 | fatal("%s: parse key: %s", __func__, ssh_err(r)); |
2129 | fp = sshkey_fingerprint(key, options.fingerprint_hash, | 2340 | fp = sshkey_fingerprint(key, options.fingerprint_hash, |
2130 | SSH_FP_DEFAULT); | 2341 | SSH_FP_DEFAULT); |
@@ -2140,47 +2351,107 @@ client_input_hostkeys(void) | |||
2140 | __func__, sshkey_ssh_name(key)); | 2351 | __func__, sshkey_ssh_name(key)); |
2141 | continue; | 2352 | continue; |
2142 | } | 2353 | } |
2143 | if ((tmp = reallocarray(keys, nkeys + 1, | 2354 | /* Skip certs */ |
2144 | sizeof(*keys))) == NULL) | 2355 | if (sshkey_is_cert(key)) { |
2145 | fatal("%s: reallocarray failed nkeys = %u", | 2356 | debug3("%s: %s key is a certificate; skipping", |
2146 | __func__, nkeys); | 2357 | __func__, sshkey_ssh_name(key)); |
2147 | keys = tmp; | 2358 | continue; |
2148 | keys[nkeys++] = key; | 2359 | } |
2360 | /* Ensure keys are unique */ | ||
2361 | for (i = 0; i < ctx->nkeys; i++) { | ||
2362 | if (sshkey_equal(key, ctx->keys[i])) { | ||
2363 | error("%s: received duplicated %s host key", | ||
2364 | __func__, sshkey_ssh_name(key)); | ||
2365 | goto out; | ||
2366 | } | ||
2367 | } | ||
2368 | /* Key is good, record it */ | ||
2369 | if ((tmp = reallocarray(ctx->keys, ctx->nkeys + 1, | ||
2370 | sizeof(*ctx->keys))) == NULL) | ||
2371 | fatal("%s: reallocarray failed nkeys = %zu", | ||
2372 | __func__, ctx->nkeys); | ||
2373 | ctx->keys = tmp; | ||
2374 | ctx->keys[ctx->nkeys++] = key; | ||
2149 | key = NULL; | 2375 | key = NULL; |
2150 | } | 2376 | } |
2151 | 2377 | ||
2152 | if (nkeys == 0) { | 2378 | if (ctx->nkeys == 0) { |
2153 | error("%s: server sent no hostkeys", __func__); | 2379 | error("%s: server sent no hostkeys", __func__); |
2154 | goto out; | 2380 | goto out; |
2155 | } | 2381 | } |
2382 | if ((ctx->keys_seen = calloc(ctx->nkeys, | ||
2383 | sizeof(*ctx->keys_seen))) == NULL) | ||
2384 | fatal("%s: calloc failed", __func__); | ||
2156 | 2385 | ||
2157 | get_hostfile_hostname_ipaddr(host, | 2386 | get_hostfile_hostname_ipaddr(host, |
2158 | options.check_host_ip ? (struct sockaddr *)&hostaddr : NULL, | 2387 | options.check_host_ip ? (struct sockaddr *)&hostaddr : NULL, |
2159 | options.port, &host_str, options.check_host_ip ? &ip_str : NULL); | 2388 | options.port, &ctx->host_str, |
2389 | options.check_host_ip ? &ctx->ip_str : NULL); | ||
2390 | |||
2391 | /* Find which keys we already know about. */ | ||
2392 | if ((r = hostkeys_foreach(options.user_hostfiles[0], hostkeys_find, | ||
2393 | ctx, ctx->host_str, ctx->ip_str, | ||
2394 | HKF_WANT_PARSE_KEY|HKF_WANT_MATCH)) != 0) { | ||
2395 | error("%s: hostkeys_foreach failed: %s", __func__, ssh_err(r)); | ||
2396 | goto out; | ||
2397 | } | ||
2398 | |||
2399 | /* Figure out if we have any new keys to add */ | ||
2400 | ctx->nnew = 0; | ||
2401 | for (i = 0; i < ctx->nkeys; i++) { | ||
2402 | if (!ctx->keys_seen[i]) | ||
2403 | ctx->nnew++; | ||
2404 | } | ||
2160 | 2405 | ||
2161 | debug3("%s: update known hosts for %s%s%s with %u keys from server", | 2406 | debug3("%s: %zu keys from server: %zu new, %zu retained. %zu to remove", |
2162 | __func__, host_str, | 2407 | __func__, ctx->nkeys, ctx->nnew, ctx->nkeys - ctx->nnew, ctx->nold); |
2163 | options.check_host_ip ? " " : "", | ||
2164 | options.check_host_ip ? ip_str : "", nkeys); | ||
2165 | 2408 | ||
2166 | if ((r = hostfile_replace_entries(options.user_hostfiles[0], | 2409 | if (ctx->nnew == 0 && ctx->nold != 0) { |
2167 | host_str, options.check_host_ip ? ip_str : NULL, | 2410 | /* We have some keys to remove. Just do it. */ |
2168 | keys, nkeys, options.hash_known_hosts, 0, | 2411 | update_known_hosts(ctx); |
2169 | options.fingerprint_hash)) != 0) { | 2412 | } else if (ctx->nnew != 0) { |
2170 | error("%s: hostfile_replace_entries failed: %s", | 2413 | /* |
2171 | __func__, ssh_err(r)); | 2414 | * We have received hitherto-unseen keys from the server. |
2172 | goto out; | 2415 | * Ask the server to confirm ownership of the private halves. |
2416 | */ | ||
2417 | debug3("%s: asking server to prove ownership for %zu keys", | ||
2418 | __func__, ctx->nnew); | ||
2419 | if ((r = sshpkt_start(ssh, SSH2_MSG_GLOBAL_REQUEST)) != 0 || | ||
2420 | (r = sshpkt_put_cstring(ssh, | ||
2421 | "hostkeys-prove@openssh.com")) != 0 || | ||
2422 | (r = sshpkt_put_u8(ssh, 1)) != 0) /* bool: want reply */ | ||
2423 | fatal("%s: cannot prepare packet: %s", | ||
2424 | __func__, ssh_err(r)); | ||
2425 | if ((buf = sshbuf_new()) == NULL) | ||
2426 | fatal("%s: sshbuf_new", __func__); | ||
2427 | for (i = 0; i < ctx->nkeys; i++) { | ||
2428 | if (ctx->keys_seen[i]) | ||
2429 | continue; | ||
2430 | sshbuf_reset(buf); | ||
2431 | if ((r = sshkey_putb(ctx->keys[i], buf)) != 0) | ||
2432 | fatal("%s: sshkey_putb: %s", | ||
2433 | __func__, ssh_err(r)); | ||
2434 | if ((r = sshpkt_put_stringb(ssh, buf)) != 0) | ||
2435 | fatal("%s: sshpkt_put_string: %s", | ||
2436 | __func__, ssh_err(r)); | ||
2437 | } | ||
2438 | if ((r = sshpkt_send(ssh)) != 0) | ||
2439 | fatal("%s: sshpkt_send: %s", __func__, ssh_err(r)); | ||
2440 | client_register_global_confirm( | ||
2441 | client_global_hostkeys_private_confirm, ctx); | ||
2442 | ctx = NULL; /* will be freed in callback */ | ||
2173 | } | 2443 | } |
2174 | 2444 | ||
2175 | /* Success */ | 2445 | /* Success */ |
2176 | out: | 2446 | out: |
2177 | free(host_str); | 2447 | hostkeys_update_ctx_free(ctx); |
2178 | free(ip_str); | ||
2179 | sshkey_free(key); | 2448 | sshkey_free(key); |
2180 | for (i = 0; i < nkeys; i++) | ||
2181 | sshkey_free(keys[i]); | ||
2182 | sshbuf_free(buf); | 2449 | sshbuf_free(buf); |
2183 | return success; | 2450 | /* |
2451 | * NB. Return success for all cases. The server doesn't need to know | ||
2452 | * what the client does with its hosts file. | ||
2453 | */ | ||
2454 | return 1; | ||
2184 | } | 2455 | } |
2185 | 2456 | ||
2186 | static int | 2457 | static int |