upstream commit

Host key rotation support.

Add a hostkeys@openssh.com protocol extension (global request) for
a server to inform a client of all its available host key after
authentication has completed. The client may record the keys in
known_hosts, allowing it to upgrade to better host key algorithms
and a server to gracefully rotate its keys.

The client side of this is controlled by a UpdateHostkeys config
option (default on).

ok markus@
This commit is contained in:
djm@openbsd.org 2015-01-26 03:04:45 +00:00 committed by Damien Miller
parent 60b1825262
commit 8d4f87258f
9 changed files with 404 additions and 31 deletions

View File

@ -282,6 +282,28 @@ by the client cancel the forwarding of a Unix domain socket.
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
@ -406,4 +428,4 @@ respond with a SSH_FXP_STATUS message.
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 $

View File

@ -1,4 +1,4 @@
/* $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
@ -112,6 +112,7 @@
#include "msg.h"
#include "roaming.h"
#include "ssherr.h"
#include "hostfile.h"
/* import options */
extern Options options;
@ -1781,6 +1782,7 @@ client_input_exit_status(int type, u_int32_t seq, void *ctxt)
quit_pending = 1;
return 0;
}
static int
client_input_agent_open(int type, u_int32_t seq, void *ctxt)
{
@ -2038,6 +2040,7 @@ client_input_channel_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)
{
@ -2085,6 +2088,91 @@ 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)
{
@ -2092,10 +2180,12 @@ 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);

View File

@ -1,4 +1,4 @@
/* $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
@ -39,6 +39,7 @@
#include "includes.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
@ -49,6 +50,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include "xmalloc.h"
#include "match.h"
@ -430,6 +432,29 @@ lookup_key_in_hostkeys_by_type(struct hostkeys *hostkeys, int keytype,
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.
@ -439,34 +464,183 @@ add_host_to_hostfile(const char *filename, const char *host,
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;
if (store_hash) {
if ((hashed_host = host_hash(host, NULL, 0)) == NULL) {
error("%s: host_hash failed", __func__);
fclose(f);
return 0;
}
}
fprintf(f, "%s ", store_hash ? hashed_host : host);
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);
success = write_host_entry(f, host, key, store_hash);
fclose(f);
return success;
}
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;
}
/* 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,
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
match_maybe_hashed(const char *host, const char *names, int *was_hashed)
{

View File

@ -1,4 +1,4 @@
/* $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>
@ -44,6 +44,9 @@ int hostfile_read_key(char **, u_int *, struct sshkey *);
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 '|'

View File

@ -1,4 +1,4 @@
/* $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
@ -156,7 +156,7 @@ typedef enum {
oCanonicalDomains, oCanonicalizeHostname, oCanonicalizeMaxDots,
oCanonicalizeFallbackLocal, oCanonicalizePermittedCNAMEs,
oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys,
oFingerprintHash,
oFingerprintHash, oUpdateHostkeys,
oIgnoredUnknownOption, oDeprecated, oUnsupported
} OpCodes;
@ -273,6 +273,7 @@ static struct {
{ "streamlocalbindunlink", oStreamLocalBindUnlink },
{ "revokedhostkeys", oRevokedHostKeys },
{ "fingerprinthash", oFingerprintHash },
{ "updatehostkeys", oUpdateHostkeys },
{ "ignoreunknown", oIgnoreUnknown },
{ NULL, oBadOption }
@ -1476,6 +1477,10 @@ parse_int:
*intptr = value;
break;
case oUpdateHostkeys:
intptr = &options->update_hostkeys;
goto parse_flag;
case oDeprecated:
debug("%s line %d: Deprecated option \"%s\"",
filename, linenum, keyword);
@ -1654,6 +1659,7 @@ initialize_options(Options * options)
options->canonicalize_hostname = -1;
options->revoked_host_keys = NULL;
options->fingerprint_hash = -1;
options->update_hostkeys = -1;
}
/*
@ -1833,6 +1839,8 @@ fill_default_options(Options * options)
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 { \
@ -2256,6 +2264,7 @@ dump_client_config(Options *o, const char *host)
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);

View File

@ -1,4 +1,4 @@
/* $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>
@ -146,7 +146,9 @@ typedef struct {
char *revoked_host_keys;
int fingerprint_hash;
int fingerprint_hash;
int update_hostkeys;
char *ignored_unknown; /* Pattern list of unknown tokens to ignore */
} Options;

View File

@ -33,8 +33,8 @@
.\" (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
@ -1492,6 +1492,28 @@ is not specified, it defaults to
.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

View File

@ -1,4 +1,4 @@
/* $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
@ -818,6 +818,7 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port,
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;
@ -926,6 +927,7 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port,
free(ra);
free(fp);
}
hostkey_trusted = 1;
break;
case HOST_NEW:
if (options.host_key_alias == NULL && port != 0 &&
@ -989,6 +991,7 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port,
free(fp);
if (!confirm(msg))
goto fail;
hostkey_trusted = 1; /* user explicitly confirmed */
}
/*
* If not in strict mode, add the key automatically to the
@ -1187,6 +1190,12 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port,
}
}
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)

44
sshd.c
View File

@ -1,4 +1,4 @@
/* $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
@ -911,6 +911,42 @@ get_hostkey_index(Key *key, struct ssh *ssh)
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
@ -1722,6 +1758,8 @@ main(int ac, char **av)
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;
@ -2185,6 +2223,10 @@ main(int ac, char **av)
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);