diff --git a/readconf.c b/readconf.c index 12995a184..97c0d183d 100644 --- a/readconf.c +++ b/readconf.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -172,7 +172,7 @@ typedef enum { oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys, oFingerprintHash, oUpdateHostkeys, oHostbasedKeyTypes, oPubkeyAcceptedKeyTypes, oCASignatureAlgorithms, oProxyJump, - oSecurityKeyProvider, + oSecurityKeyProvider, oKnownHostsCommand, oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported } OpCodes; @@ -311,6 +311,7 @@ static struct { { "ignoreunknown", oIgnoreUnknown }, { "proxyjump", oProxyJump }, { "securitykeyprovider", oSecurityKeyProvider }, + { "knownhostscommand", oKnownHostsCommand }, { NULL, oBadOption } }; @@ -1254,6 +1255,10 @@ parse_char_array: charptr = &options->sk_provider; goto parse_string; + case oKnownHostsCommand: + charptr = &options->known_hosts_command; + goto parse_command; + case oProxyCommand: charptr = &options->proxy_command; /* Ignore ProxyCommand if ProxyJump already specified */ @@ -2217,6 +2222,7 @@ initialize_options(Options * options) options->update_hostkeys = -1; options->hostbased_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->pkcs11_provider); CLEAR_ON_NONE(options->sk_provider); + CLEAR_ON_NONE(options->known_hosts_command); if (options->jump_host != NULL && strcmp(options->jump_host, "none") == 0 && 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(oRevokedHostKeys, o->revoked_host_keys); dump_cfg_string(oXAuthLocation, o->xauth_location); + dump_cfg_string(oKnownHostsCommand, o->known_hosts_command); /* Forwards */ dump_cfg_forwards(oDynamicForward, o->num_local_forwards, o->local_forwards); diff --git a/readconf.h b/readconf.h index 268dbf179..85ea2e112 100644 --- a/readconf.h +++ b/readconf.h @@ -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 @@ -169,6 +169,8 @@ typedef struct { int jump_port; char *jump_extra; + char *known_hosts_command; + char *ignored_unknown; /* Pattern list of unknown tokens to ignore */ } Options; diff --git a/ssh.1 b/ssh.1 index 555317887..81e147c75 100644 --- a/ssh.1 +++ b/ssh.1 @@ -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.1,v 1.414 2020/07/15 05:40:05 jmc Exp $ -.Dd $Mdocdate: July 15 2020 $ +.\" $OpenBSD: ssh.1,v 1.415 2020/12/22 00:15:23 djm Exp $ +.Dd $Mdocdate: December 22 2020 $ .Dt SSH 1 .Os .Sh NAME @@ -521,6 +521,7 @@ For full details of the options listed below, and their possible values, see .It KbdInteractiveAuthentication .It KbdInteractiveDevices .It KexAlgorithms +.It KnownHostsCommand .It LocalCommand .It LocalForward .It LogLevel diff --git a/ssh_config.5 b/ssh_config.5 index 98035a2f4..d6d22f1dd 100644 --- a/ssh_config.5 +++ b/ssh_config.5 @@ -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.338 2020/10/16 14:34:33 jmc Exp $ -.Dd $Mdocdate: October 16 2020 $ +.\" $OpenBSD: ssh_config.5,v 1.339 2020/12/22 00:15:23 djm Exp $ +.Dd $Mdocdate: December 22 2020 $ .Dt SSH_CONFIG 5 .Os .Sh NAME @@ -1120,6 +1120,31 @@ diffie-hellman-group14-sha256 .Pp The list of available key exchange algorithms may also be obtained using .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 Specifies a command to execute on the local machine after successfully connecting to the server. @@ -1883,10 +1908,31 @@ A literal Hash of %l%h%p%r. .It %d 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 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 The local user ID. +.It %K +The base64 encoded host key. .It %k The host key alias if specified, otherwise the orignal remote hostname given on the command line. @@ -1909,6 +1955,9 @@ network interface assigned if tunnel forwarding was requested, or .Qq NONE otherwise. +.It %t +The type of the server host key, e.g. +.Cm ssh-ed25519 .It %u The local username. .El @@ -1917,6 +1966,7 @@ The local username. .Cm ControlPath , .Cm IdentityAgent , .Cm IdentityFile , +.Cm KnownHostsCommand , .Cm LocalForward , .Cm Match exec , .Cm RemoteCommand , @@ -1925,6 +1975,9 @@ and .Cm UserKnownHostsFile accept the tokens %%, %C, %d, %h, %i, %L, %l, %n, %p, %r, and %u. .Pp +.Cm KnownHostsCommand +additionally accepts the tokens %f, %H, %I, %K and %t. +.Pp .Cm Hostname accepts the tokens %% and %h. .Pp @@ -1948,6 +2001,7 @@ The keywords .Cm ControlPath , .Cm IdentityAgent , .Cm IdentityFile +.Cm KnownHostsCommand , and .Cm UserKnownHostsFile support environment variables. diff --git a/sshconnect.c b/sshconnect.c index 6e7f83430..616ee37e8 100644 --- a/sshconnect.c +++ b/sshconnect.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -865,6 +865,84 @@ other_hostkeys_message(const char *host, const char *ip, 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 * 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 sshkey *host_key, int readonly, int clobber_port, 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; 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); for (i = 0; i < num_system_hostfiles; i++) 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; 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); for (i = 0; i < num_system_hostfiles; i++) 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: @@ -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_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; /* @@ -993,6 +1084,11 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo, debug3_f("host key found in GlobalKnownHostsFile; " "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 (readonly || want_cert) 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, host_key, ROQUIET, 1, 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"); 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, 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: sshkey_free(plain); diff --git a/sshconnect.h b/sshconnect.h index 161056b4d..f518a9a13 100644 --- a/sshconnect.h +++ b/sshconnect.h @@ -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. @@ -88,3 +88,7 @@ int ssh_local_cmd(const char *); void maybe_add_key_to_agent(const char *, struct sshkey *, const char *, const char *); + +void load_hostkeys_command(struct hostkeys *, const char *, + const char *, const struct ssh_conn_info *, + const struct sshkey *, const char *); diff --git a/sshconnect2.c b/sshconnect2.c index 4460bca85..95813b9b8 100644 --- a/sshconnect2.c +++ b/sshconnect2.c @@ -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) 2008 Damien Miller. All rights reserved. @@ -137,6 +137,10 @@ order_hostkeyalgs(char *host, struct sockaddr *hostaddr, u_short port, load_hostkeys(hostkeys, hostname, 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 * 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); if (*first != '\0') debug3_f("prefer hostkeyalgs: %s", first); - + else + debug3_f("no algorithms matched; accept original"); out: free(best); free(first);