boolean FALSE
string socket path
+2.5. connection: hostkey update and rotation "hostkeys@openssh.com"
+
+OpenSSH supports a protocol extension allowing a server to inform
+a client of all its protocol v.2 hostkeys after user-authentication
+has completed.
+
+ byte SSH_MSG_GLOBAL_REQUEST
+ string "hostkeys@openssh.com"
+ string[] hostkeys
+
+Upon receiving this message, a client may update its known_hosts
+file, adding keys that it has not seen before and deleting keys
+for the server host that are no longer offered.
+
+This extension allows a client to learn key types that it had
+not previously encountered, thereby allowing it to potentially
+upgrade from weaker key algorithms to better ones. It also
+supports graceful key rotation: a server may offer multiple keys
+of the same type for a period (to give clients an opportunity to
+learn them using this extension) before removing the deprecated
+key from those offered.
+
3. SFTP protocol changes
3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK
This extension is advertised in the SSH_FXP_VERSION hello with version
"1".
-$OpenBSD: PROTOCOL,v 1.24 2014/07/15 15:54:14 millert Exp $
+$OpenBSD: PROTOCOL,v 1.25 2015/01/26 03:04:45 djm Exp $
-/* $OpenBSD: clientloop.c,v 1.266 2015/01/20 23:14:00 deraadt Exp $ */
+/* $OpenBSD: clientloop.c,v 1.267 2015/01/26 03:04:45 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
#include "msg.h"
#include "roaming.h"
#include "ssherr.h"
+#include "hostfile.h"
/* import options */
extern Options options;
quit_pending = 1;
return 0;
}
+
static int
client_input_agent_open(int type, u_int32_t seq, void *ctxt)
{
free(ctype);
return 0;
}
+
static int
client_input_channel_req(int type, u_int32_t seq, void *ctxt)
{
free(rtype);
return 0;
}
+
+/*
+ * Handle hostkeys@openssh.com global request to inform the client of all
+ * the server's hostkeys. The keys are checked against the user's
+ * HostkeyAlgorithms preference before they are accepted.
+ */
+static int
+client_input_hostkeys(void)
+{
+ const u_char *blob = NULL;
+ u_int i, len = 0, nkeys = 0;
+ struct sshbuf *buf = NULL;
+ struct sshkey *key = NULL, **tmp, **keys = NULL;
+ int r, success = 1;
+ char *fp, *host_str = NULL;
+ static int hostkeys_seen = 0; /* XXX use struct ssh */
+
+ /*
+ * NB. Return success for all cases other than protocol error. The
+ * server doesn't need to know what the client does with its hosts
+ * file.
+ */
+
+ blob = packet_get_string_ptr(&len);
+ packet_check_eom();
+
+ if (hostkeys_seen)
+ fatal("%s: server already sent hostkeys", __func__);
+ if (!options.update_hostkeys || options.num_user_hostfiles <= 0)
+ return 1;
+ if ((buf = sshbuf_from(blob, len)) == NULL)
+ fatal("%s: sshbuf_from failed", __func__);
+ while (sshbuf_len(buf) > 0) {
+ sshkey_free(key);
+ key = NULL;
+ if ((r = sshkey_froms(buf, &key)) != 0)
+ fatal("%s: parse key: %s", __func__, ssh_err(r));
+ fp = sshkey_fingerprint(key, options.fingerprint_hash,
+ SSH_FP_DEFAULT);
+ debug3("%s: received %s key %s", __func__,
+ sshkey_type(key), fp);
+ free(fp);
+ /* Check that the key is accepted in HostkeyAlgorithms */
+ if (options.hostkeyalgorithms != NULL &&
+ match_pattern_list(sshkey_ssh_name(key),
+ options.hostkeyalgorithms,
+ strlen(options.hostkeyalgorithms), 0) != 1) {
+ debug3("%s: %s key not permitted by HostkeyAlgorithms",
+ __func__, sshkey_ssh_name(key));
+ continue;
+ }
+ if ((tmp = reallocarray(keys, nkeys + 1,
+ sizeof(*keys))) == NULL)
+ fatal("%s: reallocarray failed nkeys = %u",
+ __func__, nkeys);
+ keys = tmp;
+ keys[nkeys++] = key;
+ key = NULL;
+ }
+
+ debug3("%s: received %u keys from server", __func__, nkeys);
+ if (nkeys == 0) {
+ error("%s: server sent no hostkeys", __func__);
+ goto out;
+ }
+
+ get_hostfile_hostname_ipaddr(host, NULL, options.port, &host_str, NULL);
+
+ if ((r = hostfile_replace_entries(options.user_hostfiles[0], host_str,
+ keys, nkeys, options.hash_known_hosts, 1)) != 0) {
+ error("%s: hostfile_replace_entries failed: %s",
+ __func__, ssh_err(r));
+ goto out;
+ }
+
+ /* Success */
+ out:
+ free(host_str);
+ sshkey_free(key);
+ for (i = 0; i < nkeys; i++)
+ sshkey_free(keys[i]);
+ sshbuf_free(buf);
+ return success;
+}
+
static int
client_input_global_request(int type, u_int32_t seq, void *ctxt)
{
int want_reply;
int success = 0;
- rtype = packet_get_string(NULL);
+ rtype = packet_get_cstring(NULL);
want_reply = packet_get_char();
debug("client_input_global_request: rtype %s want_reply %d",
rtype, want_reply);
+ if (strcmp(rtype, "hostkeys@openssh.com") == 0)
+ success = client_input_hostkeys();
if (want_reply) {
packet_start(success ?
SSH2_MSG_REQUEST_SUCCESS : SSH2_MSG_REQUEST_FAILURE);
-/* $OpenBSD: hostfile.c,v 1.61 2015/01/18 21:48:09 djm Exp $ */
+/* $OpenBSD: hostfile.c,v 1.62 2015/01/26 03:04:45 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
*/
#include <sys/types.h>
+#include <sys/stat.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
+#include <unistd.h>
#include "xmalloc.h"
#include "match.h"
found) == HOST_FOUND);
}
+static int
+write_host_entry(FILE *f, const char *host,
+ const struct sshkey *key, int store_hash)
+{
+ int r, success = 0;
+ char *hashed_host = NULL;
+
+ if (store_hash) {
+ if ((hashed_host = host_hash(host, NULL, 0)) == NULL) {
+ error("%s: host_hash failed", __func__);
+ return 0;
+ }
+ }
+ fprintf(f, "%s ", store_hash ? hashed_host : host);
+
+ if ((r = sshkey_write(key, f)) == 0)
+ success = 1;
+ else
+ error("%s: sshkey_write failed: %s", __func__, ssh_err(r));
+ fputc('\n', f);
+ return success;
+}
+
/*
* Appends an entry to the host file. Returns false if the entry could not
* be appended.
const struct sshkey *key, int store_hash)
{
FILE *f;
- int r, success = 0;
- char *hashed_host = NULL;
+ int success;
if (key == NULL)
return 1; /* XXX ? */
f = fopen(filename, "a");
if (!f)
return 0;
+ success = write_host_entry(f, host, key, store_hash);
+ fclose(f);
+ return success;
+}
- if (store_hash) {
- if ((hashed_host = host_hash(host, NULL, 0)) == NULL) {
- error("%s: host_hash failed", __func__);
- fclose(f);
+struct host_delete_ctx {
+ FILE *out;
+ int quiet;
+ const char *host;
+ int *skip_keys;
+ struct sshkey * const *keys;
+ size_t nkeys;
+};
+
+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_INFO;
+ size_t i;
+
+ if (l->status == HKF_STATUS_HOST_MATCHED) {
+ if (l->marker != MRK_NONE) {
+ /* Don't remove CA and revocation lines */
+ fprintf(ctx->out, "%s\n", l->line);
+ return 0;
+ }
+
+ /* XXX might need a knob for this later */
+ /* Don't remove RSA1 keys */
+ if (l->key->type == KEY_RSA1) {
+ fprintf(ctx->out, "%s\n", l->line);
return 0;
}
+
+ /*
+ * 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)) {
+ ctx->skip_keys[i] = 1;
+ 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: Host %s removed",
+ ctx->quiet ? __func__ : "", ctx->quiet ? ": " : "",
+ l->path, l->linenum, ctx->host);
+ return 0;
}
- fprintf(f, "%s ", store_hash ? hashed_host : host);
+ /* 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;
+}
- if ((r = sshkey_write(key, f)) != 0) {
- error("%s: saving key in %s failed: %s",
- __func__, filename, ssh_err(r));
- } else
- success = 1;
- fputc('\n', f);
- fclose(f);
- return success;
+int
+hostfile_replace_entries(const char *filename, const char *host,
+ struct sshkey **keys, size_t nkeys, int store_hash, int quiet)
+{
+ int r, fd, oerrno = 0;
+ int loglevel = quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_INFO;
+ struct host_delete_ctx ctx;
+ char *temp = NULL, *back = NULL;
+ mode_t omask;
+ size_t i;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.host = host;
+ ctx.quiet = quiet;
+ if ((ctx.skip_keys = calloc(nkeys, sizeof(*ctx.skip_keys))) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+ ctx.keys = keys;
+ ctx.nkeys = nkeys;
+
+ /*
+ * Prepare temporary file for in-place deletion.
+ */
+ if ((r = asprintf(&temp, "%s.XXXXXXXXXXX", filename)) < 0 ||
+ (r = asprintf(&back, "%s.old", filename)) < 0) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto fail;
+ }
+
+ omask = umask(077);
+ 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 all entries for the specified host from the file */
+ if ((r = hostkeys_foreach(filename, host_delete, &ctx, host,
+ HKF_WANT_PARSE_KEY)) != 0) {
+ error("%s: hostkeys_foreach failed: %s", __func__, ssh_err(r));
+ goto fail;
+ }
+
+ /* Add the requested keys */
+ for (i = 0; i < nkeys; i++) {
+ if (ctx.skip_keys[i])
+ continue;
+ do_log2(loglevel, "%s%sadd %s key to %s",
+ quiet ? __func__ : "", quiet ? ": " : NULL,
+ sshkey_type(keys[i]), filename);
+ if (!write_host_entry(ctx.out, host, keys[i], store_hash)) {
+ r = SSH_ERR_INTERNAL_ERROR;
+ goto fail;
+ }
+ }
+ fclose(ctx.out);
+ ctx.out = NULL;
+
+ /* 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;
+ }
+ /* success */
+ r = 0;
+ fail:
+ if (temp != NULL && r != 0)
+ unlink(temp);
+ free(temp);
+ free(back);
+ if (ctx.out != NULL)
+ fclose(ctx.out);
+ free(ctx.skip_keys);
+ if (r == SSH_ERR_SYSTEM_ERROR)
+ errno = oerrno;
+ return r;
}
static int
-/* $OpenBSD: hostfile.h,v 1.22 2015/01/18 21:40:24 djm Exp $ */
+/* $OpenBSD: hostfile.h,v 1.23 2015/01/26 03:04:45 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
int add_host_to_hostfile(const char *, const char *,
const struct sshkey *, int);
+int hostfile_replace_entries(const char *filename, const char *host,
+ struct sshkey **keys, size_t nkeys, int store_hash, int quiet);
+
#define HASH_MAGIC "|1|"
#define HASH_DELIM '|'
-/* $OpenBSD: readconf.c,v 1.228 2015/01/16 06:40:12 deraadt Exp $ */
+/* $OpenBSD: readconf.c,v 1.229 2015/01/26 03:04:45 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
oCanonicalDomains, oCanonicalizeHostname, oCanonicalizeMaxDots,
oCanonicalizeFallbackLocal, oCanonicalizePermittedCNAMEs,
oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys,
- oFingerprintHash,
+ oFingerprintHash, oUpdateHostkeys,
oIgnoredUnknownOption, oDeprecated, oUnsupported
} OpCodes;
{ "streamlocalbindunlink", oStreamLocalBindUnlink },
{ "revokedhostkeys", oRevokedHostKeys },
{ "fingerprinthash", oFingerprintHash },
+ { "updatehostkeys", oUpdateHostkeys },
{ "ignoreunknown", oIgnoreUnknown },
{ NULL, oBadOption }
*intptr = value;
break;
+ case oUpdateHostkeys:
+ intptr = &options->update_hostkeys;
+ goto parse_flag;
+
case oDeprecated:
debug("%s line %d: Deprecated option \"%s\"",
filename, linenum, keyword);
options->canonicalize_hostname = -1;
options->revoked_host_keys = NULL;
options->fingerprint_hash = -1;
+ options->update_hostkeys = -1;
}
/*
options->canonicalize_hostname = SSH_CANONICALISE_NO;
if (options->fingerprint_hash == -1)
options->fingerprint_hash = SSH_FP_HASH_DEFAULT;
+ if (options->update_hostkeys == -1)
+ options->update_hostkeys = 1;
#define CLEAR_ON_NONE(v) \
do { \
dump_cfg_fmtint(oUsePrivilegedPort, o->use_privileged_port);
dump_cfg_fmtint(oVerifyHostKeyDNS, o->verify_host_key_dns);
dump_cfg_fmtint(oVisualHostKey, o->visual_host_key);
+ dump_cfg_fmtint(oUpdateHostkeys, o->update_hostkeys);
/* Integer options */
dump_cfg_int(oCanonicalizeMaxDots, o->canonicalize_max_dots);
-/* $OpenBSD: readconf.h,v 1.106 2015/01/15 09:40:00 djm Exp $ */
+/* $OpenBSD: readconf.h,v 1.107 2015/01/26 03:04:45 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
char *revoked_host_keys;
- int fingerprint_hash;
+ int fingerprint_hash;
+
+ int update_hostkeys;
char *ignored_unknown; /* Pattern list of unknown tokens to ignore */
} Options;
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\"
-.\" $OpenBSD: ssh_config.5,v 1.199 2014/12/22 09:24:59 jmc Exp $
-.Dd $Mdocdate: December 22 2014 $
+.\" $OpenBSD: ssh_config.5,v 1.200 2015/01/26 03:04:45 djm Exp $
+.Dd $Mdocdate: January 26 2015 $
.Dt SSH_CONFIG 5
.Os
.Sh NAME
.Dq any .
The default is
.Dq any:any .
+.It Cm UpdateHostkeys
+Specifies whether
+.Xr ssh 1
+should accept notifications of additional hostkeys from the server sent
+after authentication has completed and add them to
+.Cm UserKnownHostsFile .
+The argument must be
+.Dq yes
+(the default)
+or
+.Dq no .
+Enabling this option allows learning alternate hostkeys for a server
+and supports graceful key rotation by allowing a server to public replacement
+keys before old ones are removed.
+Additional hostkeys are only accepted if the key used to authenticate the
+host was already trusted or explicity accepted by the user.
+.Pp
+Presently, only
+.Xr sshd 8
+from OpenSSH 6.8 and greater support the
+.Dq hostkeys@openssh.com
+protocol extension used to inform the client of all the server's hostkeys.
.It Cm UsePrivilegedPort
Specifies whether to use a privileged port for outgoing connections.
The argument must be
-/* $OpenBSD: sshconnect.c,v 1.256 2015/01/20 23:14:00 deraadt Exp $ */
+/* $OpenBSD: sshconnect.c,v 1.257 2015/01/26 03:04:46 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
int len, cancelled_forwarding = 0;
int local = sockaddr_is_local(hostaddr);
int r, want_cert = key_is_cert(host_key), host_ip_differ = 0;
+ int hostkey_trusted = 0; /* Known or explicitly accepted by user */
struct hostkeys *host_hostkeys, *ip_hostkeys;
u_int i;
free(ra);
free(fp);
}
+ hostkey_trusted = 1;
break;
case HOST_NEW:
if (options.host_key_alias == NULL && port != 0 &&
free(fp);
if (!confirm(msg))
goto fail;
+ hostkey_trusted = 1; /* user explicitly confirmed */
}
/*
* If not in strict mode, add the key automatically to the
}
}
+ if (!hostkey_trusted && options.update_hostkeys) {
+ debug("%s: hostkey not known or explicitly trusted: "
+ "disabling UpdateHostkeys", __func__);
+ options.update_hostkeys = 0;
+ }
+
free(ip);
free(host);
if (host_hostkeys != NULL)
-/* $OpenBSD: sshd.c,v 1.438 2015/01/20 23:14:00 deraadt Exp $ */
+/* $OpenBSD: sshd.c,v 1.439 2015/01/26 03:04:46 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
return (-1);
}
+/* Inform the client of all hostkeys */
+static void
+notify_hostkeys(struct ssh *ssh)
+{
+ struct sshbuf *buf;
+ struct sshkey *key;
+ int i, nkeys, r;
+ char *fp;
+
+ if ((buf = sshbuf_new()) == NULL)
+ fatal("%s: sshbuf_new", __func__);
+ for (i = nkeys = 0; i < options.num_host_key_files; i++) {
+ key = get_hostkey_public_by_index(i, ssh);
+ if (key == NULL || key->type == KEY_UNSPEC ||
+ key->type == KEY_RSA1 || sshkey_is_cert(key))
+ continue;
+ fp = sshkey_fingerprint(key, options.fingerprint_hash,
+ SSH_FP_DEFAULT);
+ debug3("%s: key %d: %s %s", __func__, i,
+ sshkey_ssh_name(key), fp);
+ free(fp);
+ if ((r = sshkey_puts(key, buf)) != 0)
+ fatal("%s: couldn't put hostkey %d: %s",
+ __func__, i, ssh_err(r));
+ nkeys++;
+ }
+ if (nkeys == 0)
+ fatal("%s: no hostkeys", __func__);
+ debug3("%s: send %d hostkeys", __func__, nkeys);
+ packet_start(SSH2_MSG_GLOBAL_REQUEST);
+ packet_put_cstring("hostkeys@openssh.com");
+ packet_put_char(0); /* want-reply */
+ packet_put_string(sshbuf_ptr(buf), sshbuf_len(buf));
+ packet_send();
+}
+
/*
* returns 1 if connection should be dropped, 0 otherwise.
* dropping starts at connection #max_startups_begin with a probability
continue;
key = key_load_private(options.host_key_files[i], "", NULL);
pubkey = key_load_public(options.host_key_files[i], NULL);
+ if (pubkey == NULL && key != NULL)
+ pubkey = key_demote(key);
sensitive_data.host_keys[i] = key;
sensitive_data.host_pubkeys[i] = pubkey;
packet_set_timeout(options.client_alive_interval,
options.client_alive_count_max);
+ /* Try to send all our hostkeys to the client */
+ if (compat20)
+ notify_hostkeys(active_state);
+
/* Start session. */
do_authenticated(authctxt);