mirror of
git://anongit.mindrot.org/openssh.git
synced 2024-11-27 05:46:36 +08:00
d98f14b532
When preparing to update the known_hosts file, fully check both entries for both the host and the address (if CheckHostIP enabled) and ensure that, at the end of the operation, entries for both are recorded. Make sure this works with HashKnownHosts too, which requires maintaining a list of entry-types seen across the whole file for each key. ok markus@ OpenBSD-Commit-ID: 374dc263103f6b343d9671f87dbf81ffd0d6abdd
913 lines
24 KiB
C
913 lines
24 KiB
C
/* $OpenBSD: hostfile.c,v 1.85 2020/10/11 22:13:37 djm Exp $ */
|
|
/*
|
|
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
|
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
|
* All rights reserved
|
|
* Functions for manipulating the known hosts files.
|
|
*
|
|
* As far as I am concerned, the code I have written for this software
|
|
* can be used freely for any purpose. Any derived versions of this
|
|
* software must be clearly marked as such, and if the derived work is
|
|
* incompatible with the protocol description in the RFC file, it must be
|
|
* called by a name other than "ssh" or "Secure Shell".
|
|
*
|
|
*
|
|
* Copyright (c) 1999, 2000 Markus Friedl. All rights reserved.
|
|
* Copyright (c) 1999 Niels Provos. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "includes.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <netinet/in.h>
|
|
|
|
#include <errno.h>
|
|
#include <resolv.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "xmalloc.h"
|
|
#include "match.h"
|
|
#include "sshkey.h"
|
|
#include "hostfile.h"
|
|
#include "log.h"
|
|
#include "misc.h"
|
|
#include "pathnames.h"
|
|
#include "ssherr.h"
|
|
#include "digest.h"
|
|
#include "hmac.h"
|
|
|
|
struct hostkeys {
|
|
struct hostkey_entry *entries;
|
|
u_int num_entries;
|
|
};
|
|
|
|
/* XXX hmac is too easy to dictionary attack; use bcrypt? */
|
|
|
|
static int
|
|
extract_salt(const char *s, u_int l, u_char *salt, size_t salt_len)
|
|
{
|
|
char *p, *b64salt;
|
|
u_int b64len;
|
|
int ret;
|
|
|
|
if (l < sizeof(HASH_MAGIC) - 1) {
|
|
debug2("extract_salt: string too short");
|
|
return (-1);
|
|
}
|
|
if (strncmp(s, HASH_MAGIC, sizeof(HASH_MAGIC) - 1) != 0) {
|
|
debug2("extract_salt: invalid magic identifier");
|
|
return (-1);
|
|
}
|
|
s += sizeof(HASH_MAGIC) - 1;
|
|
l -= sizeof(HASH_MAGIC) - 1;
|
|
if ((p = memchr(s, HASH_DELIM, l)) == NULL) {
|
|
debug2("extract_salt: missing salt termination character");
|
|
return (-1);
|
|
}
|
|
|
|
b64len = p - s;
|
|
/* Sanity check */
|
|
if (b64len == 0 || b64len > 1024) {
|
|
debug2("extract_salt: bad encoded salt length %u", b64len);
|
|
return (-1);
|
|
}
|
|
b64salt = xmalloc(1 + b64len);
|
|
memcpy(b64salt, s, b64len);
|
|
b64salt[b64len] = '\0';
|
|
|
|
ret = __b64_pton(b64salt, salt, salt_len);
|
|
free(b64salt);
|
|
if (ret == -1) {
|
|
debug2("extract_salt: salt decode error");
|
|
return (-1);
|
|
}
|
|
if (ret != (int)ssh_hmac_bytes(SSH_DIGEST_SHA1)) {
|
|
debug2("extract_salt: expected salt len %zd, got %d",
|
|
ssh_hmac_bytes(SSH_DIGEST_SHA1), ret);
|
|
return (-1);
|
|
}
|
|
|
|
return (0);
|
|
}
|
|
|
|
char *
|
|
host_hash(const char *host, const char *name_from_hostfile, u_int src_len)
|
|
{
|
|
struct ssh_hmac_ctx *ctx;
|
|
u_char salt[256], result[256];
|
|
char uu_salt[512], uu_result[512];
|
|
static char encoded[1024];
|
|
u_int len;
|
|
|
|
len = ssh_digest_bytes(SSH_DIGEST_SHA1);
|
|
|
|
if (name_from_hostfile == NULL) {
|
|
/* Create new salt */
|
|
arc4random_buf(salt, len);
|
|
} else {
|
|
/* Extract salt from known host entry */
|
|
if (extract_salt(name_from_hostfile, src_len, salt,
|
|
sizeof(salt)) == -1)
|
|
return (NULL);
|
|
}
|
|
|
|
if ((ctx = ssh_hmac_start(SSH_DIGEST_SHA1)) == NULL ||
|
|
ssh_hmac_init(ctx, salt, len) < 0 ||
|
|
ssh_hmac_update(ctx, host, strlen(host)) < 0 ||
|
|
ssh_hmac_final(ctx, result, sizeof(result)))
|
|
fatal("%s: ssh_hmac failed", __func__);
|
|
ssh_hmac_free(ctx);
|
|
|
|
if (__b64_ntop(salt, len, uu_salt, sizeof(uu_salt)) == -1 ||
|
|
__b64_ntop(result, len, uu_result, sizeof(uu_result)) == -1)
|
|
fatal("%s: __b64_ntop failed", __func__);
|
|
|
|
snprintf(encoded, sizeof(encoded), "%s%s%c%s", HASH_MAGIC, uu_salt,
|
|
HASH_DELIM, uu_result);
|
|
|
|
return (encoded);
|
|
}
|
|
|
|
/*
|
|
* Parses an RSA (number of bits, e, n) or DSA key from a string. Moves the
|
|
* pointer over the key. Skips any whitespace at the beginning and at end.
|
|
*/
|
|
|
|
int
|
|
hostfile_read_key(char **cpp, u_int *bitsp, struct sshkey *ret)
|
|
{
|
|
char *cp;
|
|
|
|
/* Skip leading whitespace. */
|
|
for (cp = *cpp; *cp == ' ' || *cp == '\t'; cp++)
|
|
;
|
|
|
|
if (sshkey_read(ret, &cp) != 0)
|
|
return 0;
|
|
|
|
/* Skip trailing whitespace. */
|
|
for (; *cp == ' ' || *cp == '\t'; cp++)
|
|
;
|
|
|
|
/* Return results. */
|
|
*cpp = cp;
|
|
if (bitsp != NULL)
|
|
*bitsp = sshkey_size(ret);
|
|
return 1;
|
|
}
|
|
|
|
static HostkeyMarker
|
|
check_markers(char **cpp)
|
|
{
|
|
char marker[32], *sp, *cp = *cpp;
|
|
int ret = MRK_NONE;
|
|
|
|
while (*cp == '@') {
|
|
/* Only one marker is allowed */
|
|
if (ret != MRK_NONE)
|
|
return MRK_ERROR;
|
|
/* Markers are terminated by whitespace */
|
|
if ((sp = strchr(cp, ' ')) == NULL &&
|
|
(sp = strchr(cp, '\t')) == NULL)
|
|
return MRK_ERROR;
|
|
/* Extract marker for comparison */
|
|
if (sp <= cp + 1 || sp >= cp + sizeof(marker))
|
|
return MRK_ERROR;
|
|
memcpy(marker, cp, sp - cp);
|
|
marker[sp - cp] = '\0';
|
|
if (strcmp(marker, CA_MARKER) == 0)
|
|
ret = MRK_CA;
|
|
else if (strcmp(marker, REVOKE_MARKER) == 0)
|
|
ret = MRK_REVOKE;
|
|
else
|
|
return MRK_ERROR;
|
|
|
|
/* Skip past marker and any whitespace that follows it */
|
|
cp = sp;
|
|
for (; *cp == ' ' || *cp == '\t'; cp++)
|
|
;
|
|
}
|
|
*cpp = cp;
|
|
return ret;
|
|
}
|
|
|
|
struct hostkeys *
|
|
init_hostkeys(void)
|
|
{
|
|
struct hostkeys *ret = xcalloc(1, sizeof(*ret));
|
|
|
|
ret->entries = NULL;
|
|
return ret;
|
|
}
|
|
|
|
struct load_callback_ctx {
|
|
const char *host;
|
|
u_long num_loaded;
|
|
struct hostkeys *hostkeys;
|
|
};
|
|
|
|
static int
|
|
record_hostkey(struct hostkey_foreach_line *l, void *_ctx)
|
|
{
|
|
struct load_callback_ctx *ctx = (struct load_callback_ctx *)_ctx;
|
|
struct hostkeys *hostkeys = ctx->hostkeys;
|
|
struct hostkey_entry *tmp;
|
|
|
|
if (l->status == HKF_STATUS_INVALID) {
|
|
/* XXX make this verbose() in the future */
|
|
debug("%s:%ld: parse error in hostkeys file",
|
|
l->path, l->linenum);
|
|
return 0;
|
|
}
|
|
|
|
debug3("%s: found %skey type %s in file %s:%lu", __func__,
|
|
l->marker == MRK_NONE ? "" :
|
|
(l->marker == MRK_CA ? "ca " : "revoked "),
|
|
sshkey_type(l->key), l->path, l->linenum);
|
|
if ((tmp = recallocarray(hostkeys->entries, hostkeys->num_entries,
|
|
hostkeys->num_entries + 1, sizeof(*hostkeys->entries))) == NULL)
|
|
return SSH_ERR_ALLOC_FAIL;
|
|
hostkeys->entries = tmp;
|
|
hostkeys->entries[hostkeys->num_entries].host = xstrdup(ctx->host);
|
|
hostkeys->entries[hostkeys->num_entries].file = xstrdup(l->path);
|
|
hostkeys->entries[hostkeys->num_entries].line = l->linenum;
|
|
hostkeys->entries[hostkeys->num_entries].key = l->key;
|
|
l->key = NULL; /* steal it */
|
|
hostkeys->entries[hostkeys->num_entries].marker = l->marker;
|
|
hostkeys->num_entries++;
|
|
ctx->num_loaded++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
load_hostkeys(struct hostkeys *hostkeys, const char *host, const char *path)
|
|
{
|
|
int r;
|
|
struct load_callback_ctx ctx;
|
|
|
|
ctx.host = host;
|
|
ctx.num_loaded = 0;
|
|
ctx.hostkeys = hostkeys;
|
|
|
|
if ((r = hostkeys_foreach(path, record_hostkey, &ctx, host, NULL,
|
|
HKF_WANT_MATCH|HKF_WANT_PARSE_KEY)) != 0) {
|
|
if (r != SSH_ERR_SYSTEM_ERROR && errno != ENOENT)
|
|
debug("%s: hostkeys_foreach failed for %s: %s",
|
|
__func__, path, ssh_err(r));
|
|
}
|
|
if (ctx.num_loaded != 0)
|
|
debug3("%s: loaded %lu keys from %s", __func__,
|
|
ctx.num_loaded, host);
|
|
}
|
|
|
|
void
|
|
free_hostkeys(struct hostkeys *hostkeys)
|
|
{
|
|
u_int i;
|
|
|
|
for (i = 0; i < hostkeys->num_entries; i++) {
|
|
free(hostkeys->entries[i].host);
|
|
free(hostkeys->entries[i].file);
|
|
sshkey_free(hostkeys->entries[i].key);
|
|
explicit_bzero(hostkeys->entries + i, sizeof(*hostkeys->entries));
|
|
}
|
|
free(hostkeys->entries);
|
|
freezero(hostkeys, sizeof(*hostkeys));
|
|
}
|
|
|
|
static int
|
|
check_key_not_revoked(struct hostkeys *hostkeys, struct sshkey *k)
|
|
{
|
|
int is_cert = sshkey_is_cert(k);
|
|
u_int i;
|
|
|
|
for (i = 0; i < hostkeys->num_entries; i++) {
|
|
if (hostkeys->entries[i].marker != MRK_REVOKE)
|
|
continue;
|
|
if (sshkey_equal_public(k, hostkeys->entries[i].key))
|
|
return -1;
|
|
if (is_cert && k != NULL &&
|
|
sshkey_equal_public(k->cert->signature_key,
|
|
hostkeys->entries[i].key))
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Match keys against a specified key, or look one up by key type.
|
|
*
|
|
* If looking for a keytype (key == NULL) and one is found then return
|
|
* HOST_FOUND, otherwise HOST_NEW.
|
|
*
|
|
* If looking for a key (key != NULL):
|
|
* 1. If the key is a cert and a matching CA is found, return HOST_OK
|
|
* 2. If the key is not a cert and a matching key is found, return HOST_OK
|
|
* 3. If no key matches but a key with a different type is found, then
|
|
* return HOST_CHANGED
|
|
* 4. If no matching keys are found, then return HOST_NEW.
|
|
*
|
|
* Finally, check any found key is not revoked.
|
|
*/
|
|
static HostStatus
|
|
check_hostkeys_by_key_or_type(struct hostkeys *hostkeys,
|
|
struct sshkey *k, int keytype, int nid, const struct hostkey_entry **found)
|
|
{
|
|
u_int i;
|
|
HostStatus end_return = HOST_NEW;
|
|
int want_cert = sshkey_is_cert(k);
|
|
HostkeyMarker want_marker = want_cert ? MRK_CA : MRK_NONE;
|
|
|
|
if (found != NULL)
|
|
*found = NULL;
|
|
|
|
for (i = 0; i < hostkeys->num_entries; i++) {
|
|
if (hostkeys->entries[i].marker != want_marker)
|
|
continue;
|
|
if (k == NULL) {
|
|
if (hostkeys->entries[i].key->type != keytype)
|
|
continue;
|
|
if (nid != -1 &&
|
|
sshkey_type_plain(keytype) == KEY_ECDSA &&
|
|
hostkeys->entries[i].key->ecdsa_nid != nid)
|
|
continue;
|
|
end_return = HOST_FOUND;
|
|
if (found != NULL)
|
|
*found = hostkeys->entries + i;
|
|
k = hostkeys->entries[i].key;
|
|
break;
|
|
}
|
|
if (want_cert) {
|
|
if (sshkey_equal_public(k->cert->signature_key,
|
|
hostkeys->entries[i].key)) {
|
|
/* A matching CA exists */
|
|
end_return = HOST_OK;
|
|
if (found != NULL)
|
|
*found = hostkeys->entries + i;
|
|
break;
|
|
}
|
|
} else {
|
|
if (sshkey_equal(k, hostkeys->entries[i].key)) {
|
|
end_return = HOST_OK;
|
|
if (found != NULL)
|
|
*found = hostkeys->entries + i;
|
|
break;
|
|
}
|
|
/* A non-maching key exists */
|
|
end_return = HOST_CHANGED;
|
|
if (found != NULL)
|
|
*found = hostkeys->entries + i;
|
|
}
|
|
}
|
|
if (check_key_not_revoked(hostkeys, k) != 0) {
|
|
end_return = HOST_REVOKED;
|
|
if (found != NULL)
|
|
*found = NULL;
|
|
}
|
|
return end_return;
|
|
}
|
|
|
|
HostStatus
|
|
check_key_in_hostkeys(struct hostkeys *hostkeys, struct sshkey *key,
|
|
const struct hostkey_entry **found)
|
|
{
|
|
if (key == NULL)
|
|
fatal("no key to look up");
|
|
return check_hostkeys_by_key_or_type(hostkeys, key, 0, -1, found);
|
|
}
|
|
|
|
int
|
|
lookup_key_in_hostkeys_by_type(struct hostkeys *hostkeys, int keytype, int nid,
|
|
const struct hostkey_entry **found)
|
|
{
|
|
return (check_hostkeys_by_key_or_type(hostkeys, NULL, keytype, nid,
|
|
found) == HOST_FOUND);
|
|
}
|
|
|
|
int
|
|
lookup_marker_in_hostkeys(struct hostkeys *hostkeys, int want_marker)
|
|
{
|
|
u_int i;
|
|
|
|
for (i = 0; i < hostkeys->num_entries; i++) {
|
|
if (hostkeys->entries[i].marker == (HostkeyMarker)want_marker)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
write_host_entry(FILE *f, const char *host, const char *ip,
|
|
const struct sshkey *key, int store_hash)
|
|
{
|
|
int r, success = 0;
|
|
char *hashed_host = NULL, *lhost;
|
|
|
|
lhost = xstrdup(host);
|
|
lowercase(lhost);
|
|
|
|
if (store_hash) {
|
|
if ((hashed_host = host_hash(lhost, NULL, 0)) == NULL) {
|
|
error("%s: host_hash failed", __func__);
|
|
free(lhost);
|
|
return 0;
|
|
}
|
|
fprintf(f, "%s ", hashed_host);
|
|
} else if (ip != NULL)
|
|
fprintf(f, "%s,%s ", lhost, ip);
|
|
else {
|
|
fprintf(f, "%s ", lhost);
|
|
}
|
|
free(lhost);
|
|
if ((r = sshkey_write(key, f)) == 0)
|
|
success = 1;
|
|
else
|
|
error("%s: sshkey_write failed: %s", __func__, ssh_err(r));
|
|
fputc('\n', f);
|
|
/* If hashing is enabled, the IP address needs to go on its own line */
|
|
if (success && store_hash && ip != NULL)
|
|
success = write_host_entry(f, ip, NULL, key, 1);
|
|
return success;
|
|
}
|
|
|
|
/*
|
|
* Create user ~/.ssh directory if it doesn't exist and we want to write to it.
|
|
* If notify is set, a message will be emitted if the directory is created.
|
|
*/
|
|
void
|
|
hostfile_create_user_ssh_dir(const char *filename, int notify)
|
|
{
|
|
char *dotsshdir = NULL, *p;
|
|
size_t len;
|
|
struct stat st;
|
|
|
|
if ((p = strrchr(filename, '/')) == NULL)
|
|
return;
|
|
len = p - filename;
|
|
dotsshdir = tilde_expand_filename("~/" _PATH_SSH_USER_DIR, getuid());
|
|
if (strlen(dotsshdir) > len || strncmp(filename, dotsshdir, len) != 0)
|
|
goto out; /* not ~/.ssh prefixed */
|
|
if (stat(dotsshdir, &st) == 0)
|
|
goto out; /* dir already exists */
|
|
else if (errno != ENOENT)
|
|
error("Could not stat %s: %s", dotsshdir, strerror(errno));
|
|
else {
|
|
#ifdef WITH_SELINUX
|
|
ssh_selinux_setfscreatecon(dotsshdir);
|
|
#endif
|
|
if (mkdir(dotsshdir, 0700) == -1)
|
|
error("Could not create directory '%.200s' (%s).",
|
|
dotsshdir, strerror(errno));
|
|
else if (notify)
|
|
logit("Created directory '%s'.", dotsshdir);
|
|
#ifdef WITH_SELINUX
|
|
ssh_selinux_setfscreatecon(NULL);
|
|
#endif
|
|
}
|
|
out:
|
|
free(dotsshdir);
|
|
}
|
|
|
|
/*
|
|
* Appends an entry to the host file. Returns false if the entry could not
|
|
* be appended.
|
|
*/
|
|
int
|
|
add_host_to_hostfile(const char *filename, const char *host,
|
|
const struct sshkey *key, int store_hash)
|
|
{
|
|
FILE *f;
|
|
int success;
|
|
|
|
if (key == NULL)
|
|
return 1; /* XXX ? */
|
|
hostfile_create_user_ssh_dir(filename, 0);
|
|
f = fopen(filename, "a");
|
|
if (!f)
|
|
return 0;
|
|
success = write_host_entry(f, host, NULL, key, store_hash);
|
|
fclose(f);
|
|
return success;
|
|
}
|
|
|
|
struct host_delete_ctx {
|
|
FILE *out;
|
|
int quiet;
|
|
const char *host, *ip;
|
|
u_int *match_keys; /* mask of HKF_MATCH_* for this key */
|
|
struct sshkey * const *keys;
|
|
size_t nkeys;
|
|
int modified;
|
|
};
|
|
|
|
static int
|
|
host_delete(struct hostkey_foreach_line *l, void *_ctx)
|
|
{
|
|
struct host_delete_ctx *ctx = (struct host_delete_ctx *)_ctx;
|
|
int loglevel = ctx->quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_VERBOSE;
|
|
size_t i;
|
|
|
|
/* Don't remove CA and revocation lines */
|
|
if (l->status == HKF_STATUS_MATCHED && l->marker == MRK_NONE) {
|
|
/*
|
|
* If this line contains one of the keys that we will be
|
|
* adding later, then don't change it and mark the key for
|
|
* skipping.
|
|
*/
|
|
for (i = 0; i < ctx->nkeys; i++) {
|
|
if (!sshkey_equal(ctx->keys[i], l->key))
|
|
continue;
|
|
ctx->match_keys[i] |= l->match;
|
|
fprintf(ctx->out, "%s\n", l->line);
|
|
debug3("%s: %s key already at %s:%ld", __func__,
|
|
sshkey_type(l->key), l->path, l->linenum);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Hostname matches and has no CA/revoke marker, delete it
|
|
* by *not* writing the line to ctx->out.
|
|
*/
|
|
do_log2(loglevel, "%s%s%s:%ld: Removed %s key for host %s",
|
|
ctx->quiet ? __func__ : "", ctx->quiet ? ": " : "",
|
|
l->path, l->linenum, sshkey_type(l->key), ctx->host);
|
|
ctx->modified = 1;
|
|
return 0;
|
|
}
|
|
/* Retain non-matching hosts and invalid lines when deleting */
|
|
if (l->status == HKF_STATUS_INVALID) {
|
|
do_log2(loglevel, "%s%s%s:%ld: invalid known_hosts entry",
|
|
ctx->quiet ? __func__ : "", ctx->quiet ? ": " : "",
|
|
l->path, l->linenum);
|
|
}
|
|
fprintf(ctx->out, "%s\n", l->line);
|
|
return 0;
|
|
}
|
|
|
|
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)
|
|
{
|
|
int r, fd, oerrno = 0;
|
|
int loglevel = quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_VERBOSE;
|
|
struct host_delete_ctx ctx;
|
|
char *fp, *temp = NULL, *back = NULL;
|
|
const char *what;
|
|
mode_t omask;
|
|
size_t i;
|
|
u_int want;
|
|
|
|
omask = umask(077);
|
|
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
ctx.host = host;
|
|
ctx.ip = ip;
|
|
ctx.quiet = quiet;
|
|
|
|
if ((ctx.match_keys = calloc(nkeys, sizeof(*ctx.match_keys))) == NULL)
|
|
return SSH_ERR_ALLOC_FAIL;
|
|
ctx.keys = keys;
|
|
ctx.nkeys = nkeys;
|
|
ctx.modified = 0;
|
|
|
|
/*
|
|
* Prepare temporary file for in-place deletion.
|
|
*/
|
|
if ((r = asprintf(&temp, "%s.XXXXXXXXXXX", filename)) == -1 ||
|
|
(r = asprintf(&back, "%s.old", filename)) == -1) {
|
|
r = SSH_ERR_ALLOC_FAIL;
|
|
goto fail;
|
|
}
|
|
|
|
if ((fd = mkstemp(temp)) == -1) {
|
|
oerrno = errno;
|
|
error("%s: mkstemp: %s", __func__, strerror(oerrno));
|
|
r = SSH_ERR_SYSTEM_ERROR;
|
|
goto fail;
|
|
}
|
|
if ((ctx.out = fdopen(fd, "w")) == NULL) {
|
|
oerrno = errno;
|
|
close(fd);
|
|
error("%s: fdopen: %s", __func__, strerror(oerrno));
|
|
r = SSH_ERR_SYSTEM_ERROR;
|
|
goto fail;
|
|
}
|
|
|
|
/* Remove stale/mismatching entries for the specified host */
|
|
if ((r = hostkeys_foreach(filename, host_delete, &ctx, host, ip,
|
|
HKF_WANT_PARSE_KEY)) != 0) {
|
|
oerrno = errno;
|
|
error("%s: hostkeys_foreach failed: %s", __func__, ssh_err(r));
|
|
goto fail;
|
|
}
|
|
|
|
/* Re-add the requested keys */
|
|
want = HKF_MATCH_HOST | (ip == NULL ? 0 : HKF_MATCH_IP);
|
|
for (i = 0; i < nkeys; i++) {
|
|
if ((want & ctx.match_keys[i]) == want)
|
|
continue;
|
|
if ((fp = sshkey_fingerprint(keys[i], hash_alg,
|
|
SSH_FP_DEFAULT)) == NULL) {
|
|
r = SSH_ERR_ALLOC_FAIL;
|
|
goto fail;
|
|
}
|
|
/* write host/ip */
|
|
what = "";
|
|
if (ctx.match_keys[i] == 0) {
|
|
what = "Adding new key";
|
|
if (!write_host_entry(ctx.out, host, ip,
|
|
keys[i], store_hash)) {
|
|
r = SSH_ERR_INTERNAL_ERROR;
|
|
goto fail;
|
|
}
|
|
} else if ((want & ~ctx.match_keys[i]) == HKF_MATCH_HOST) {
|
|
what = "Fixing match (hostname)";
|
|
if (!write_host_entry(ctx.out, host, NULL,
|
|
keys[i], store_hash)) {
|
|
r = SSH_ERR_INTERNAL_ERROR;
|
|
goto fail;
|
|
}
|
|
} else if ((want & ~ctx.match_keys[i]) == HKF_MATCH_IP) {
|
|
what = "Fixing match (address)";
|
|
if (!write_host_entry(ctx.out, ip, NULL,
|
|
keys[i], store_hash)) {
|
|
r = SSH_ERR_INTERNAL_ERROR;
|
|
goto fail;
|
|
}
|
|
}
|
|
do_log2(loglevel, "%s%s%s for %s%s%s to %s: %s %s",
|
|
quiet ? __func__ : "", quiet ? ": " : "", what,
|
|
host, ip == NULL ? "" : ",", ip == NULL ? "" : ip, filename,
|
|
sshkey_ssh_name(keys[i]), fp);
|
|
free(fp);
|
|
ctx.modified = 1;
|
|
}
|
|
fclose(ctx.out);
|
|
ctx.out = NULL;
|
|
|
|
if (ctx.modified) {
|
|
/* Backup the original file and replace it with the temporary */
|
|
if (unlink(back) == -1 && errno != ENOENT) {
|
|
oerrno = errno;
|
|
error("%s: unlink %.100s: %s", __func__,
|
|
back, strerror(errno));
|
|
r = SSH_ERR_SYSTEM_ERROR;
|
|
goto fail;
|
|
}
|
|
if (link(filename, back) == -1) {
|
|
oerrno = errno;
|
|
error("%s: link %.100s to %.100s: %s", __func__,
|
|
filename, back, strerror(errno));
|
|
r = SSH_ERR_SYSTEM_ERROR;
|
|
goto fail;
|
|
}
|
|
if (rename(temp, filename) == -1) {
|
|
oerrno = errno;
|
|
error("%s: rename \"%s\" to \"%s\": %s", __func__,
|
|
temp, filename, strerror(errno));
|
|
r = SSH_ERR_SYSTEM_ERROR;
|
|
goto fail;
|
|
}
|
|
} else {
|
|
/* No changes made; just delete the temporary file */
|
|
if (unlink(temp) != 0)
|
|
error("%s: unlink \"%s\": %s", __func__,
|
|
temp, strerror(errno));
|
|
}
|
|
|
|
/* success */
|
|
r = 0;
|
|
fail:
|
|
if (temp != NULL && r != 0)
|
|
unlink(temp);
|
|
free(temp);
|
|
free(back);
|
|
if (ctx.out != NULL)
|
|
fclose(ctx.out);
|
|
free(ctx.match_keys);
|
|
umask(omask);
|
|
if (r == SSH_ERR_SYSTEM_ERROR)
|
|
errno = oerrno;
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
match_maybe_hashed(const char *host, const char *names, int *was_hashed)
|
|
{
|
|
int hashed = *names == HASH_DELIM;
|
|
const char *hashed_host;
|
|
size_t nlen = strlen(names);
|
|
|
|
if (was_hashed != NULL)
|
|
*was_hashed = hashed;
|
|
if (hashed) {
|
|
if ((hashed_host = host_hash(host, names, nlen)) == NULL)
|
|
return -1;
|
|
return nlen == strlen(hashed_host) &&
|
|
strncmp(hashed_host, names, nlen) == 0;
|
|
}
|
|
return match_hostname(host, names) == 1;
|
|
}
|
|
|
|
int
|
|
hostkeys_foreach(const char *path, hostkeys_foreach_fn *callback, void *ctx,
|
|
const char *host, const char *ip, u_int options)
|
|
{
|
|
FILE *f;
|
|
char *line = NULL, ktype[128];
|
|
u_long linenum = 0;
|
|
char *cp, *cp2;
|
|
u_int kbits;
|
|
int hashed;
|
|
int s, r = 0;
|
|
struct hostkey_foreach_line lineinfo;
|
|
size_t linesize = 0, l;
|
|
|
|
memset(&lineinfo, 0, sizeof(lineinfo));
|
|
if (host == NULL && (options & HKF_WANT_MATCH) != 0)
|
|
return SSH_ERR_INVALID_ARGUMENT;
|
|
if ((f = fopen(path, "r")) == NULL)
|
|
return SSH_ERR_SYSTEM_ERROR;
|
|
|
|
debug3("%s: reading file \"%s\"", __func__, path);
|
|
while (getline(&line, &linesize, f) != -1) {
|
|
linenum++;
|
|
line[strcspn(line, "\n")] = '\0';
|
|
|
|
free(lineinfo.line);
|
|
sshkey_free(lineinfo.key);
|
|
memset(&lineinfo, 0, sizeof(lineinfo));
|
|
lineinfo.path = path;
|
|
lineinfo.linenum = linenum;
|
|
lineinfo.line = xstrdup(line);
|
|
lineinfo.marker = MRK_NONE;
|
|
lineinfo.status = HKF_STATUS_OK;
|
|
lineinfo.keytype = KEY_UNSPEC;
|
|
|
|
/* Skip any leading whitespace, comments and empty lines. */
|
|
for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
|
|
;
|
|
if (!*cp || *cp == '#' || *cp == '\n') {
|
|
if ((options & HKF_WANT_MATCH) == 0) {
|
|
lineinfo.status = HKF_STATUS_COMMENT;
|
|
if ((r = callback(&lineinfo, ctx)) != 0)
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ((lineinfo.marker = check_markers(&cp)) == MRK_ERROR) {
|
|
verbose("%s: invalid marker at %s:%lu",
|
|
__func__, path, linenum);
|
|
if ((options & HKF_WANT_MATCH) == 0)
|
|
goto bad;
|
|
continue;
|
|
}
|
|
|
|
/* Find the end of the host name portion. */
|
|
for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t'; cp2++)
|
|
;
|
|
lineinfo.hosts = cp;
|
|
*cp2++ = '\0';
|
|
|
|
/* Check if the host name matches. */
|
|
if (host != NULL) {
|
|
if ((s = match_maybe_hashed(host, lineinfo.hosts,
|
|
&hashed)) == -1) {
|
|
debug2("%s: %s:%ld: bad host hash \"%.32s\"",
|
|
__func__, path, linenum, lineinfo.hosts);
|
|
goto bad;
|
|
}
|
|
if (s == 1) {
|
|
lineinfo.status = HKF_STATUS_MATCHED;
|
|
lineinfo.match |= HKF_MATCH_HOST |
|
|
(hashed ? HKF_MATCH_HOST_HASHED : 0);
|
|
}
|
|
/* Try matching IP address if supplied */
|
|
if (ip != NULL) {
|
|
if ((s = match_maybe_hashed(ip, lineinfo.hosts,
|
|
&hashed)) == -1) {
|
|
debug2("%s: %s:%ld: bad ip hash "
|
|
"\"%.32s\"", __func__, path,
|
|
linenum, lineinfo.hosts);
|
|
goto bad;
|
|
}
|
|
if (s == 1) {
|
|
lineinfo.status = HKF_STATUS_MATCHED;
|
|
lineinfo.match |= HKF_MATCH_IP |
|
|
(hashed ? HKF_MATCH_IP_HASHED : 0);
|
|
}
|
|
}
|
|
/*
|
|
* Skip this line if host matching requested and
|
|
* neither host nor address matched.
|
|
*/
|
|
if ((options & HKF_WANT_MATCH) != 0 &&
|
|
lineinfo.status != HKF_STATUS_MATCHED)
|
|
continue;
|
|
}
|
|
|
|
/* Got a match. Skip host name and any following whitespace */
|
|
for (; *cp2 == ' ' || *cp2 == '\t'; cp2++)
|
|
;
|
|
if (*cp2 == '\0' || *cp2 == '#') {
|
|
debug2("%s:%ld: truncated before key type",
|
|
path, linenum);
|
|
goto bad;
|
|
}
|
|
lineinfo.rawkey = cp = cp2;
|
|
|
|
if ((options & HKF_WANT_PARSE_KEY) != 0) {
|
|
/*
|
|
* Extract the key from the line. This will skip
|
|
* any leading whitespace. Ignore badly formatted
|
|
* lines.
|
|
*/
|
|
if ((lineinfo.key = sshkey_new(KEY_UNSPEC)) == NULL) {
|
|
error("%s: sshkey_new failed", __func__);
|
|
r = SSH_ERR_ALLOC_FAIL;
|
|
break;
|
|
}
|
|
if (!hostfile_read_key(&cp, &kbits, lineinfo.key)) {
|
|
goto bad;
|
|
}
|
|
lineinfo.keytype = lineinfo.key->type;
|
|
lineinfo.comment = cp;
|
|
} else {
|
|
/* Extract and parse key type */
|
|
l = strcspn(lineinfo.rawkey, " \t");
|
|
if (l <= 1 || l >= sizeof(ktype) ||
|
|
lineinfo.rawkey[l] == '\0')
|
|
goto bad;
|
|
memcpy(ktype, lineinfo.rawkey, l);
|
|
ktype[l] = '\0';
|
|
lineinfo.keytype = sshkey_type_from_name(ktype);
|
|
|
|
/*
|
|
* Assume legacy RSA1 if the first component is a short
|
|
* decimal number.
|
|
*/
|
|
if (lineinfo.keytype == KEY_UNSPEC && l < 8 &&
|
|
strspn(ktype, "0123456789") == l)
|
|
goto bad;
|
|
|
|
/*
|
|
* Check that something other than whitespace follows
|
|
* the key type. This won't catch all corruption, but
|
|
* it does catch trivial truncation.
|
|
*/
|
|
cp2 += l; /* Skip past key type */
|
|
for (; *cp2 == ' ' || *cp2 == '\t'; cp2++)
|
|
;
|
|
if (*cp2 == '\0' || *cp2 == '#') {
|
|
debug2("%s:%ld: truncated after key type",
|
|
path, linenum);
|
|
lineinfo.keytype = KEY_UNSPEC;
|
|
}
|
|
if (lineinfo.keytype == KEY_UNSPEC) {
|
|
bad:
|
|
sshkey_free(lineinfo.key);
|
|
lineinfo.key = NULL;
|
|
lineinfo.status = HKF_STATUS_INVALID;
|
|
if ((r = callback(&lineinfo, ctx)) != 0)
|
|
break;
|
|
continue;
|
|
}
|
|
}
|
|
if ((r = callback(&lineinfo, ctx)) != 0)
|
|
break;
|
|
}
|
|
sshkey_free(lineinfo.key);
|
|
free(lineinfo.line);
|
|
free(line);
|
|
fclose(f);
|
|
return r;
|
|
}
|