diff --git a/hostfile.c b/hostfile.c index 40dbbd478..5f0366310 100644 --- a/hostfile.c +++ b/hostfile.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hostfile.c,v 1.59 2015/01/15 09:40:00 djm Exp $ */ +/* $OpenBSD: hostfile.c,v 1.60 2015/01/18 21:40:23 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -42,6 +42,7 @@ #include +#include #include #include #include @@ -64,6 +65,8 @@ struct hostkeys { 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) { @@ -496,7 +499,147 @@ add_host_to_hostfile(const char *filename, const char *host, __func__, filename, ssh_err(r)); } else success = 1; - fputs("\n", f); + fputc('\n', f); fclose(f); return success; } + +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, nlen) == 1; +} + +int +hostkeys_foreach(const char *path, hostkeys_foreach_fn *callback, void *ctx, + const char *host, u_int options) +{ + FILE *f; + char line[8192], oline[8192]; + u_long linenum = 0; + char *cp, *cp2; + u_int kbits; + int s, r = 0; + struct hostkey_foreach_line lineinfo; + + memset(&lineinfo, 0, sizeof(lineinfo)); + if (host == NULL && (options & HKF_WANT_MATCH_HOST) != 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 (read_keyfile_line(f, path, line, sizeof(line), &linenum) == 0) { + line[strcspn(line, "\n")] = '\0'; + strlcpy(oline, line, sizeof(oline)); + + sshkey_free(lineinfo.key); + memset(&lineinfo, 0, sizeof(lineinfo)); + lineinfo.path = path; + lineinfo.linenum = linenum; + lineinfo.line = oline; + lineinfo.status = HKF_STATUS_OK; + + /* 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_HOST) == 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_HOST) == 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) { + s = match_maybe_hashed(host, lineinfo.hosts, + &lineinfo.was_hashed); + if (s == 1) + lineinfo.status = HKF_STATUS_HOST_MATCHED; + else if ((options & HKF_WANT_MATCH_HOST) != 0) + continue; + else if (s == -1) { + debug2("%s: %s:%ld: bad host hash \"%.32s\"", + __func__, path, linenum, lineinfo.hosts); + goto bad; + } + } + + /* 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", 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__); + return SSH_ERR_ALLOC_FAIL; + } + if (!hostfile_read_key(&cp, &kbits, lineinfo.key)) { +#ifdef WITH_SSH1 + sshkey_free(lineinfo.key); + lineinfo.key = sshkey_new(KEY_RSA1); + if (lineinfo.key == NULL) { + error("%s: sshkey_new fail", __func__); + return SSH_ERR_ALLOC_FAIL; + } + if (!hostfile_read_key(&cp, &kbits, + lineinfo.key)) + goto bad; +#else + goto bad; +#endif + } + if (!hostfile_check_key(kbits, lineinfo.key, host, + path, linenum)) { + bad: + lineinfo.status = HKF_STATUS_INVALID; + if ((r = callback(&lineinfo, ctx)) != 0) + break; + continue; + } + } + if ((r = callback(&lineinfo, ctx)) != 0) + break; + } + sshkey_free(lineinfo.key); + fclose(f); + return r; +} diff --git a/hostfile.h b/hostfile.h index d90973f42..24c3813aa 100644 --- a/hostfile.h +++ b/hostfile.h @@ -1,4 +1,4 @@ -/* $OpenBSD: hostfile.h,v 1.21 2015/01/15 09:40:00 djm Exp $ */ +/* $OpenBSD: hostfile.h,v 1.22 2015/01/18 21:40:24 djm Exp $ */ /* * Author: Tatu Ylonen @@ -52,4 +52,45 @@ int add_host_to_hostfile(const char *, const char *, char *host_hash(const char *, const char *, u_int); +/* + * Iterate through a hostkeys file, optionally parsing keys and matching + * hostnames. Allows access to the raw keyfile lines to allow + * streaming edits to the file to take place. + */ +#define HKF_WANT_MATCH_HOST (1) /* return only matching hosts */ +#define HKF_WANT_PARSE_KEY (1<<1) /* need key parsed */ + +#define HKF_STATUS_OK 1 /* Line parsed, didn't match host */ +#define HKF_STATUS_INVALID 2 /* line had parse error */ +#define HKF_STATUS_COMMENT 3 /* valid line contained no key */ +#define HKF_STATUS_HOST_MATCHED 4 /* hostname matched */ + +/* + * The callback function receives this as an argument for each matching + * hostkey line. The callback may "steal" the 'key' field by setting it to NULL. + * If a parse error occurred, then "hosts" and subsequent options may be NULL. + */ +struct hostkey_foreach_line { + const char *path; /* Path of file */ + u_long linenum; /* Line number */ + int status; /* One of HKF_STATUS_* */ + char *line; /* Entire key line; mutable by callback */ + int marker; /* CA/revocation markers; indicated by MRK_* value */ + const char *hosts; /* Raw hosts text, may be hashed or list multiple */ + int was_hashed; /* Non-zero if hostname was hashed */ + const char *rawkey; /* Text of key and any comment following it */ + struct sshkey *key; /* Key, if parsed ok and HKF_WANT_MATCH_HOST set */ + const char *comment; /* Any comment following the key */ +}; + +/* + * Callback fires for each line (or matching line if a HKF_WANT_* option + * is set). The foreach loop will terminate if the callback returns a non- + * zero exit status. + */ +typedef int hostkeys_foreach_fn(struct hostkey_foreach_line *l, void *ctx); + +int hostkeys_foreach(const char *path, hostkeys_foreach_fn *callback, void *ctx, + const char *host, u_int options); + #endif