upstream: add a ssh_config KnownHostsCommand that allows the client

to obtain known_hosts data from a command in addition to the usual files.

The command accepts bunch of %-expansions, including details of the
connection and the offered server host key. Note that the command may
be invoked up to three times per connection (see the manpage for
details).

ok markus@

OpenBSD-Commit-ID: 2433cff4fb323918ae968da6ff38feb99b4d33d0
This commit is contained in:
djm@openbsd.org 2020-12-22 00:15:22 +00:00 committed by Damien Miller
parent a34e14a5a0
commit da4bf0db94
7 changed files with 188 additions and 16 deletions

View File

@ -1,4 +1,4 @@
/* $OpenBSD: readconf.c,v 1.345 2020/12/21 09:19:53 djm Exp $ */ /* $OpenBSD: readconf.c,v 1.346 2020/12/22 00:15:22 djm Exp $ */
/* /*
* Author: Tatu Ylonen <ylo@cs.hut.fi> * Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@ -172,7 +172,7 @@ typedef enum {
oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys, oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys,
oFingerprintHash, oUpdateHostkeys, oHostbasedKeyTypes, oFingerprintHash, oUpdateHostkeys, oHostbasedKeyTypes,
oPubkeyAcceptedKeyTypes, oCASignatureAlgorithms, oProxyJump, oPubkeyAcceptedKeyTypes, oCASignatureAlgorithms, oProxyJump,
oSecurityKeyProvider, oSecurityKeyProvider, oKnownHostsCommand,
oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported
} OpCodes; } OpCodes;
@ -311,6 +311,7 @@ static struct {
{ "ignoreunknown", oIgnoreUnknown }, { "ignoreunknown", oIgnoreUnknown },
{ "proxyjump", oProxyJump }, { "proxyjump", oProxyJump },
{ "securitykeyprovider", oSecurityKeyProvider }, { "securitykeyprovider", oSecurityKeyProvider },
{ "knownhostscommand", oKnownHostsCommand },
{ NULL, oBadOption } { NULL, oBadOption }
}; };
@ -1254,6 +1255,10 @@ parse_char_array:
charptr = &options->sk_provider; charptr = &options->sk_provider;
goto parse_string; goto parse_string;
case oKnownHostsCommand:
charptr = &options->known_hosts_command;
goto parse_command;
case oProxyCommand: case oProxyCommand:
charptr = &options->proxy_command; charptr = &options->proxy_command;
/* Ignore ProxyCommand if ProxyJump already specified */ /* Ignore ProxyCommand if ProxyJump already specified */
@ -2217,6 +2222,7 @@ initialize_options(Options * options)
options->update_hostkeys = -1; options->update_hostkeys = -1;
options->hostbased_key_types = NULL; options->hostbased_key_types = NULL;
options->pubkey_key_types = NULL; options->pubkey_key_types = NULL;
options->known_hosts_command = NULL;
} }
/* /*
@ -2452,6 +2458,7 @@ fill_default_options(Options * options)
CLEAR_ON_NONE(options->revoked_host_keys); CLEAR_ON_NONE(options->revoked_host_keys);
CLEAR_ON_NONE(options->pkcs11_provider); CLEAR_ON_NONE(options->pkcs11_provider);
CLEAR_ON_NONE(options->sk_provider); CLEAR_ON_NONE(options->sk_provider);
CLEAR_ON_NONE(options->known_hosts_command);
if (options->jump_host != NULL && if (options->jump_host != NULL &&
strcmp(options->jump_host, "none") == 0 && strcmp(options->jump_host, "none") == 0 &&
options->jump_port == 0 && options->jump_user == NULL) { options->jump_port == 0 && options->jump_user == NULL) {
@ -3100,6 +3107,7 @@ dump_client_config(Options *o, const char *host)
dump_cfg_string(oPubkeyAcceptedKeyTypes, o->pubkey_key_types); dump_cfg_string(oPubkeyAcceptedKeyTypes, o->pubkey_key_types);
dump_cfg_string(oRevokedHostKeys, o->revoked_host_keys); dump_cfg_string(oRevokedHostKeys, o->revoked_host_keys);
dump_cfg_string(oXAuthLocation, o->xauth_location); dump_cfg_string(oXAuthLocation, o->xauth_location);
dump_cfg_string(oKnownHostsCommand, o->known_hosts_command);
/* Forwards */ /* Forwards */
dump_cfg_forwards(oDynamicForward, o->num_local_forwards, o->local_forwards); dump_cfg_forwards(oDynamicForward, o->num_local_forwards, o->local_forwards);

View File

@ -1,4 +1,4 @@
/* $OpenBSD: readconf.h,v 1.136 2020/12/17 23:10:27 djm Exp $ */ /* $OpenBSD: readconf.h,v 1.137 2020/12/22 00:15:23 djm Exp $ */
/* /*
* Author: Tatu Ylonen <ylo@cs.hut.fi> * Author: Tatu Ylonen <ylo@cs.hut.fi>
@ -169,6 +169,8 @@ typedef struct {
int jump_port; int jump_port;
char *jump_extra; char *jump_extra;
char *known_hosts_command;
char *ignored_unknown; /* Pattern list of unknown tokens to ignore */ char *ignored_unknown; /* Pattern list of unknown tokens to ignore */
} Options; } Options;

5
ssh.1
View File

@ -33,8 +33,8 @@
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\" .\"
.\" $OpenBSD: ssh.1,v 1.414 2020/07/15 05:40:05 jmc Exp $ .\" $OpenBSD: ssh.1,v 1.415 2020/12/22 00:15:23 djm Exp $
.Dd $Mdocdate: July 15 2020 $ .Dd $Mdocdate: December 22 2020 $
.Dt SSH 1 .Dt SSH 1
.Os .Os
.Sh NAME .Sh NAME
@ -521,6 +521,7 @@ For full details of the options listed below, and their possible values, see
.It KbdInteractiveAuthentication .It KbdInteractiveAuthentication
.It KbdInteractiveDevices .It KbdInteractiveDevices
.It KexAlgorithms .It KexAlgorithms
.It KnownHostsCommand
.It LocalCommand .It LocalCommand
.It LocalForward .It LocalForward
.It LogLevel .It LogLevel

View File

@ -33,8 +33,8 @@
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\" .\"
.\" $OpenBSD: ssh_config.5,v 1.338 2020/10/16 14:34:33 jmc Exp $ .\" $OpenBSD: ssh_config.5,v 1.339 2020/12/22 00:15:23 djm Exp $
.Dd $Mdocdate: October 16 2020 $ .Dd $Mdocdate: December 22 2020 $
.Dt SSH_CONFIG 5 .Dt SSH_CONFIG 5
.Os .Os
.Sh NAME .Sh NAME
@ -1120,6 +1120,31 @@ diffie-hellman-group14-sha256
.Pp .Pp
The list of available key exchange algorithms may also be obtained using The list of available key exchange algorithms may also be obtained using
.Qq ssh -Q kex . .Qq ssh -Q kex .
.It Cm KnownHostsCommand
Specifies a command to use to obtain a list of host keys, additional to
those listed in
.Cm UserKnownHostsFile
and
.Cm GlobalKnownHostsFile .
This command is executed after the files have been read.
It may write host keys lines to standard output in identical format to the
usual files (described in the
.Sx VERIFYING HOST KEYS
section in
.Xr ssh 1 ) .
Arguments to
.Cm KnownHostsCommand
accept the tokens described in the
.Sx TOKENS
section.
The command may be invoked multiple times per connection: when preparing
the preference list of host key algorithms to use, again to obtain the
host key for the requested host name and, if
.Cm CheckHostIP
is enabled, one more time to obtain the host key matching the server's
address.
If the command exits abnormally or returns a non-zero exit status then the
connection is terminated.
.It Cm LocalCommand .It Cm LocalCommand
Specifies a command to execute on the local machine after successfully Specifies a command to execute on the local machine after successfully
connecting to the server. connecting to the server.
@ -1883,10 +1908,31 @@ A literal
Hash of %l%h%p%r. Hash of %l%h%p%r.
.It %d .It %d
Local user's home directory. Local user's home directory.
.It %f
The fingerprint of the server's host key.
.It %H
The
.Pa known_hosts
hostname or address that is being searched for.
.It %h .It %h
The remote hostname. The remote hostname.
.It %I
A string describing the reason for a
.Cm KnownHostsCommand
execution; either
.Cm "ADDRESS"
when looking up a host by address (only when
.Cm CheckHostIP
is enabled),
.Cm "HOSTNAME"
when searching by hostname or
.Cm "ORDER"
when preparing the host key algorithm preference list to use for the
destination host.
.It %i .It %i
The local user ID. The local user ID.
.It %K
The base64 encoded host key.
.It %k .It %k
The host key alias if specified, otherwise the orignal remote hostname given The host key alias if specified, otherwise the orignal remote hostname given
on the command line. on the command line.
@ -1909,6 +1955,9 @@ network interface assigned if
tunnel forwarding was requested, or tunnel forwarding was requested, or
.Qq NONE .Qq NONE
otherwise. otherwise.
.It %t
The type of the server host key, e.g.
.Cm ssh-ed25519
.It %u .It %u
The local username. The local username.
.El .El
@ -1917,6 +1966,7 @@ The local username.
.Cm ControlPath , .Cm ControlPath ,
.Cm IdentityAgent , .Cm IdentityAgent ,
.Cm IdentityFile , .Cm IdentityFile ,
.Cm KnownHostsCommand ,
.Cm LocalForward , .Cm LocalForward ,
.Cm Match exec , .Cm Match exec ,
.Cm RemoteCommand , .Cm RemoteCommand ,
@ -1925,6 +1975,9 @@ and
.Cm UserKnownHostsFile .Cm UserKnownHostsFile
accept the tokens %%, %C, %d, %h, %i, %L, %l, %n, %p, %r, and %u. accept the tokens %%, %C, %d, %h, %i, %L, %l, %n, %p, %r, and %u.
.Pp .Pp
.Cm KnownHostsCommand
additionally accepts the tokens %f, %H, %I, %K and %t.
.Pp
.Cm Hostname .Cm Hostname
accepts the tokens %% and %h. accepts the tokens %% and %h.
.Pp .Pp
@ -1948,6 +2001,7 @@ The keywords
.Cm ControlPath , .Cm ControlPath ,
.Cm IdentityAgent , .Cm IdentityAgent ,
.Cm IdentityFile .Cm IdentityFile
.Cm KnownHostsCommand ,
and and
.Cm UserKnownHostsFile .Cm UserKnownHostsFile
support environment variables. support environment variables.

View File

@ -1,4 +1,4 @@
/* $OpenBSD: sshconnect.c,v 1.348 2020/12/20 23:40:19 djm Exp $ */ /* $OpenBSD: sshconnect.c,v 1.349 2020/12/22 00:15:23 djm Exp $ */
/* /*
* Author: Tatu Ylonen <ylo@cs.hut.fi> * Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@ -865,6 +865,84 @@ other_hostkeys_message(const char *host, const char *ip,
return ret; return ret;
} }
void
load_hostkeys_command(struct hostkeys *hostkeys, const char *command_template,
const char *invocation, const struct ssh_conn_info *cinfo,
const struct sshkey *host_key, const char *hostfile_hostname)
{
int r, i, ac = 0;
char *key_fp = NULL, *keytext = NULL, *tmp;
char *command = NULL, *tag = NULL, **av = NULL;
FILE *f = NULL;
pid_t pid;
void (*osigchld)(int);
xasprintf(&tag, "KnownHostsCommand-%s", invocation);
if (host_key != NULL) {
if ((key_fp = sshkey_fingerprint(host_key,
options.fingerprint_hash, SSH_FP_DEFAULT)) == NULL)
fatal_f("sshkey_fingerprint failed");
if ((r = sshkey_to_base64(host_key, &keytext)) != 0)
fatal_fr(r, "sshkey_to_base64 failed");
}
/*
* NB. all returns later this function should go via "out" to
* ensure the original SIGCHLD handler is restored properly.
*/
osigchld = ssh_signal(SIGCHLD, SIG_DFL);
/* Turn the command into an argument vector */
if (argv_split(command_template, &ac, &av) != 0) {
error("%s \"%s\" contains invalid quotes", tag,
command_template);
goto out;
}
if (ac == 0) {
error("%s \"%s\" yielded no arguments", tag,
command_template);
goto out;
}
for (i = 1; i < ac; i++) {
tmp = percent_dollar_expand(av[i],
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS(cinfo),
"H", hostfile_hostname,
"I", invocation,
"t", host_key == NULL ? "NONE" : sshkey_ssh_name(host_key),
"f", key_fp == NULL ? "NONE" : key_fp,
"K", keytext == NULL ? "NONE" : keytext,
(char *)NULL);
if (tmp == NULL)
fatal_f("percent_expand failed");
free(av[i]);
av[i] = tmp;
}
/* Prepare a printable command for logs, etc. */
command = argv_assemble(ac, av);
if ((pid = subprocess(tag, command, ac, av, &f,
SSH_SUBPROCESS_STDOUT_CAPTURE|SSH_SUBPROCESS_UNSAFE_PATH|
SSH_SUBPROCESS_PRESERVE_ENV, NULL, NULL, NULL)) == 0)
goto out;
load_hostkeys_file(hostkeys, hostfile_hostname, tag, f, 1);
if (exited_cleanly(pid, tag, command, 0) != 0)
fatal("KnownHostsCommand failed");
out:
if (f != NULL)
fclose(f);
ssh_signal(SIGCHLD, osigchld);
for (i = 0; i < ac; i++)
free(av[i]);
free(av);
free(tag);
free(command);
free(key_fp);
free(keytext);
}
/* /*
* check whether the supplied host key is valid, return -1 if the key * check whether the supplied host key is valid, return -1 if the key
* is not valid. user_hostfile[0] will not be updated if 'readonly' is true. * is not valid. user_hostfile[0] will not be updated if 'readonly' is true.
@ -877,7 +955,8 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
struct sockaddr *hostaddr, u_short port, struct sockaddr *hostaddr, u_short port,
struct sshkey *host_key, int readonly, int clobber_port, struct sshkey *host_key, int readonly, int clobber_port,
char **user_hostfiles, u_int num_user_hostfiles, char **user_hostfiles, u_int num_user_hostfiles,
char **system_hostfiles, u_int num_system_hostfiles) char **system_hostfiles, u_int num_system_hostfiles,
const char *hostfile_command)
{ {
HostStatus host_status = -1, ip_status = -1; HostStatus host_status = -1, ip_status = -1;
struct sshkey *raw_key = NULL; struct sshkey *raw_key = NULL;
@ -929,6 +1008,10 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
load_hostkeys(host_hostkeys, host, user_hostfiles[i], 0); load_hostkeys(host_hostkeys, host, user_hostfiles[i], 0);
for (i = 0; i < num_system_hostfiles; i++) for (i = 0; i < num_system_hostfiles; i++)
load_hostkeys(host_hostkeys, host, system_hostfiles[i], 0); load_hostkeys(host_hostkeys, host, system_hostfiles[i], 0);
if (hostfile_command != NULL && !clobber_port) {
load_hostkeys_command(host_hostkeys, hostfile_command,
"HOSTNAME", cinfo, host_key, host);
}
ip_hostkeys = NULL; ip_hostkeys = NULL;
if (!want_cert && options.check_host_ip) { if (!want_cert && options.check_host_ip) {
@ -937,6 +1020,10 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
load_hostkeys(ip_hostkeys, ip, user_hostfiles[i], 0); load_hostkeys(ip_hostkeys, ip, user_hostfiles[i], 0);
for (i = 0; i < num_system_hostfiles; i++) for (i = 0; i < num_system_hostfiles; i++)
load_hostkeys(ip_hostkeys, ip, system_hostfiles[i], 0); load_hostkeys(ip_hostkeys, ip, system_hostfiles[i], 0);
if (hostfile_command != NULL && !clobber_port) {
load_hostkeys_command(ip_hostkeys, hostfile_command,
"ADDRESS", cinfo, host_key, ip);
}
} }
retry: retry:
@ -951,8 +1038,12 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
host_status = check_key_in_hostkeys(host_hostkeys, host_key, host_status = check_key_in_hostkeys(host_hostkeys, host_key,
&host_found); &host_found);
/* If no host files were specified, then don't try to touch them */ /*
if (!readonly && num_user_hostfiles == 0) * If there are no hostfiles, or if the hostkey was found via
* KnownHostsCommand, then don't try to touch the disk.
*/
if (!readonly && (num_user_hostfiles == 0 ||
(host_found != NULL && host_found->note != 0)))
readonly = RDONLY; readonly = RDONLY;
/* /*
@ -993,6 +1084,11 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
debug3_f("host key found in GlobalKnownHostsFile; " debug3_f("host key found in GlobalKnownHostsFile; "
"disabling UpdateHostkeys"); "disabling UpdateHostkeys");
} }
if (options.update_hostkeys != 0 && host_found->note) {
options.update_hostkeys = 0;
debug3_f("host key found via KnownHostsCommand; "
"disabling UpdateHostkeys");
}
if (options.check_host_ip && ip_status == HOST_NEW) { if (options.check_host_ip && ip_status == HOST_NEW) {
if (readonly || want_cert) if (readonly || want_cert)
logit("%s host key for IP address " logit("%s host key for IP address "
@ -1028,7 +1124,8 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
if (check_host_key(hostname, cinfo, hostaddr, 0, if (check_host_key(hostname, cinfo, hostaddr, 0,
host_key, ROQUIET, 1, host_key, ROQUIET, 1,
user_hostfiles, num_user_hostfiles, user_hostfiles, num_user_hostfiles,
system_hostfiles, num_system_hostfiles) == 0) { system_hostfiles, num_system_hostfiles,
hostfile_command) == 0) {
debug("found matching key w/out port"); debug("found matching key w/out port");
break; break;
} }
@ -1438,7 +1535,8 @@ verify_host_key(char *host, struct sockaddr *hostaddr, struct sshkey *host_key,
} }
r = check_host_key(host, cinfo, hostaddr, options.port, host_key, r = check_host_key(host, cinfo, hostaddr, options.port, host_key,
RDRW, 0, options.user_hostfiles, options.num_user_hostfiles, RDRW, 0, options.user_hostfiles, options.num_user_hostfiles,
options.system_hostfiles, options.num_system_hostfiles); options.system_hostfiles, options.num_system_hostfiles,
options.known_hosts_command);
out: out:
sshkey_free(plain); sshkey_free(plain);

View File

@ -1,4 +1,4 @@
/* $OpenBSD: sshconnect.h,v 1.45 2020/12/20 23:40:19 djm Exp $ */ /* $OpenBSD: sshconnect.h,v 1.46 2020/12/22 00:15:23 djm Exp $ */
/* /*
* Copyright (c) 2000 Markus Friedl. All rights reserved. * Copyright (c) 2000 Markus Friedl. All rights reserved.
@ -88,3 +88,7 @@ int ssh_local_cmd(const char *);
void maybe_add_key_to_agent(const char *, struct sshkey *, void maybe_add_key_to_agent(const char *, struct sshkey *,
const char *, const char *); const char *, const char *);
void load_hostkeys_command(struct hostkeys *, const char *,
const char *, const struct ssh_conn_info *,
const struct sshkey *, const char *);

View File

@ -1,4 +1,4 @@
/* $OpenBSD: sshconnect2.c,v 1.338 2020/12/20 23:40:19 djm Exp $ */ /* $OpenBSD: sshconnect2.c,v 1.339 2020/12/22 00:15:23 djm Exp $ */
/* /*
* Copyright (c) 2000 Markus Friedl. All rights reserved. * Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2008 Damien Miller. All rights reserved. * Copyright (c) 2008 Damien Miller. All rights reserved.
@ -137,6 +137,10 @@ order_hostkeyalgs(char *host, struct sockaddr *hostaddr, u_short port,
load_hostkeys(hostkeys, hostname, load_hostkeys(hostkeys, hostname,
options.system_hostfiles[i], 0); options.system_hostfiles[i], 0);
} }
if (options.known_hosts_command != NULL) {
load_hostkeys_command(hostkeys, options.known_hosts_command,
"ORDER", cinfo, NULL, host);
}
/* /*
* If a plain public key exists that matches the type of the best * If a plain public key exists that matches the type of the best
* preference HostkeyAlgorithms, then use the whole list as is. * preference HostkeyAlgorithms, then use the whole list as is.
@ -198,7 +202,8 @@ order_hostkeyalgs(char *host, struct sockaddr *hostaddr, u_short port,
(*first == '\0' || *last == '\0') ? "" : ",", last); (*first == '\0' || *last == '\0') ? "" : ",", last);
if (*first != '\0') if (*first != '\0')
debug3_f("prefer hostkeyalgs: %s", first); debug3_f("prefer hostkeyalgs: %s", first);
else
debug3_f("no algorithms matched; accept original");
out: out:
free(best); free(best);
free(first); free(first);