diff --git a/config.c b/config.c index 9116f541..25b6e0ba 100644 --- a/config.c +++ b/config.c @@ -442,7 +442,7 @@ static void kexlist_handler(union control *ctrl, void *dlg, /* (kexlist assumed to contain all algorithms) */ dlg_update_start(ctrl, dlg); dlg_listbox_clear(ctrl, dlg); - for (i = 0; i < KEX_MAX; i++) { + for (i = 0; i < KEX_MAX_CONF; i++) { int k = conf_get_int_int(conf, CONF_ssh_kexlist, i); int j; const char *kstr = NULL; @@ -460,7 +460,7 @@ static void kexlist_handler(union control *ctrl, void *dlg, int i; /* Update array to match the list box. */ - for (i=0; i < KEX_MAX; i++) + for (i=0; i < KEX_MAX_CONF; i++) conf_set_int_int(conf, CONF_ssh_kexlist, i, dlg_listbox_getid(ctrl, dlg, i)); } @@ -2402,7 +2402,7 @@ void setup_config_box(struct controlbox *b, int midsession, c = ctrl_draglist(s, "Algorithm selection policy:", 's', HELPCTX(ssh_kexlist), kexlist_handler, P(NULL)); - c->listbox.height = 5; + c->listbox.height = KEX_MAX_CONF; s = ctrl_getset(b, "Connection/SSH/Kex", "repeat", "Options controlling key re-exchange"); @@ -2412,6 +2412,11 @@ void setup_config_box(struct controlbox *b, int midsession, conf_editbox_handler, I(CONF_ssh_rekey_time), I(-1)); + ctrl_editbox(s, "Minutes between GSS checks (0 for never)", NO_SHORTCUT, 20, + HELPCTX(ssh_kex_repeat), + conf_editbox_handler, + I(CONF_gssapirekey), + I(-1)); ctrl_editbox(s, "Max data before rekey (0 for no limit)", 'x', 20, HELPCTX(ssh_kex_repeat), conf_editbox_handler, diff --git a/doc/config.but b/doc/config.but index 767c1274..1a1e1ac4 100644 --- a/doc/config.but +++ b/doc/config.but @@ -1929,9 +1929,10 @@ PuTTY will prompt for a username at the time you make a connection. In some environments, such as the networks of large organisations implementing \i{single sign-on}, a more sensible default may be to use the name of the user logged in to the local operating system (if any); -this is particularly likely to be useful with \i{GSSAPI} authentication -(see \k{config-ssh-auth-gssapi}). This control allows you to change -the default behaviour. +this is particularly likely to be useful with \i{GSSAPI} key exchange +and user authentication (see \k{config-ssh-auth-gssapi} and +\k{config-ssh-kex}). This control allows you to change the default +behaviour. The current system username is displayed in the dialog as a convenience. It is not saved in the configuration; if a saved session @@ -2552,6 +2553,34 @@ If the first algorithm PuTTY finds is below the \q{warn below here} line, you will see a warning box when you make the connection, similar to that for cipher selection (see \k{config-ssh-encryption}). +\S2{config-ssh-gssapi-kex} GSSAPI-based key exchange + +PuTTY supports a set of key exchange methods that also incorporates +GSSAPI-based authentication. + +PuTTY can only perform the GSSAPI-authenticated key exchange methods +when using Kerberos V5, and not other GSSAPI mechanisms. PuTTY will +attempt to select these methods if it is configured to use GSSAPI +authentication (\k{config-ssh-auth-gssapi}), and if the user running +it has current Kerberos V5 credentials. If both of those are true, +then PuTTY will select the GSSAPI key exchange methods in preference +to any of the ordinary SSH key exchange methods configured in the +preference list. + +The advantage of doing GSSAPI authentication as part of the SSH key +exchange is that the SSH key exchange can be repeated later in the +session, and this allows your Kerberos V5 credentials (which are +typically short-lived) to be automatically re-delegated to the server +when they are refreshed on the client. (This feature is commonly +referred to as \q{cascading credentials}.) + +If your server doesn't support GSSAPI key exchange, it may still +support GSSAPI in the SSH user authentication phase. This will still +let you log in using your Kerberos credentials, but will only allow +you to delegate the credentials that are active at the beginning of +the session; they can't be refreshed automatically later, in a +long-running session. + \S{config-ssh-kex-rekey} \ii{Repeat key exchange} \cfg{winhelp-topic}{ssh.kex.repeat} @@ -2594,6 +2623,14 @@ purposes, rekeys have much the same properties as keepalives. should bear that in mind when deciding whether to turn them off.) Note, however, the the SSH \e{server} can still initiate rekeys. +\b \q{Minutes between GSSAPI cache checks}, if you're using GSSAPI key +exchange, specifies how often the GSSAPI credential cache is checked +to see whether new tickets are available for delegation, or current +ones are near expiration. If forwarding of GSSAPI credentials is +enabled, PuTTY will try to rekey as necessary to keep the delegated +credentials from expiring. Frequent checks are recommended; rekeying +only happens when needed. + \b \q{Max data before rekey} specifies the amount of data (in bytes) that is permitted to flow in either direction before a rekey is initiated. If this is set to zero, PuTTY will not rekey due to @@ -2947,7 +2984,15 @@ machine, which in principle can authenticate in many different ways but in practice is usually used with the \i{Kerberos} \i{single sign-on} protocol to implement \i{passwordless login}. -GSSAPI is only available in the SSH-2 protocol. +GSSAPI authentication is only available in the SSH-2 protocol. + +PuTTY supports two forms of GSSAPI-based authentication. In one of +them, the SSH key exchange happens in the normal way, and GSSAPI is +only involved in authenticating the user. In the other, GSSAPI-based +authentication is combined with the key exchange phase, and the SSH +authentication step has nothing left to do. If you enable GSSAPI +authentication, PuTTY will attempt both of these methods, and use +whichever the server supports. The topmost control on the GSSAPI subpanel is the checkbox labelled \q{Attempt GSSAPI authentication}. If this is disabled, GSSAPI will diff --git a/pgssapi.h b/pgssapi.h index f6370c2d..fd7a796d 100644 --- a/pgssapi.h +++ b/pgssapi.h @@ -58,6 +58,7 @@ typedef void * gss_name_t; typedef void * gss_cred_id_t; typedef OM_uint32 gss_qop_t; +typedef int gss_cred_usage_t; /* Flag bits for context-level services. */ @@ -76,6 +77,13 @@ typedef OM_uint32 gss_qop_t; #define GSS_C_INITIATE 1 #define GSS_C_ACCEPT 2 +/*- + * RFC 2744 Page 86 + * Expiration time of 2^32-1 seconds means infinite lifetime for a + * credential or security context + */ +#define GSS_C_INDEFINITE 0xfffffffful + /* Status code types for gss_display_status */ #define GSS_C_GSS_CODE 1 #define GSS_C_MECH_CODE 2 @@ -256,6 +264,13 @@ typedef OM_uint32 (GSS_CC *t_gss_get_mic) const gss_buffer_t /*message_buffer*/, gss_buffer_t /*msg_token*/); +typedef OM_uint32 (GSS_CC *t_gss_verify_mic) + (OM_uint32 * /*minor_status*/, + const gss_ctx_id_t /*context_handle*/, + const gss_buffer_t /*message_buffer*/, + const gss_buffer_t /*msg_token*/, + gss_qop_t * /*qop_state*/); + typedef OM_uint32 (GSS_CC *t_gss_display_status) (OM_uint32 * /*minor_status*/, OM_uint32 /*status_value*/, @@ -280,15 +295,37 @@ typedef OM_uint32 (GSS_CC *t_gss_release_buffer) (OM_uint32 * /*minor_status*/, gss_buffer_t /*buffer*/); +typedef OM_uint32 (GSS_CC *t_gss_acquire_cred) + (OM_uint32 * /*minor_status*/, + const gss_name_t /*desired_name*/, + OM_uint32 /*time_req*/, + const gss_OID_set /*desired_mechs*/, + gss_cred_usage_t /*cred_usage*/, + gss_cred_id_t * /*output_cred_handle*/, + gss_OID_set * /*actual_mechs*/, + OM_uint32 * /*time_rec*/); + +typedef OM_uint32 (GSS_CC *t_gss_inquire_cred_by_mech) + (OM_uint32 * /*minor_status*/, + const gss_cred_id_t /*cred_handle*/, + const gss_OID /*mech_type*/, + gss_name_t * /*name*/, + OM_uint32 * /*initiator_lifetime*/, + OM_uint32 * /*acceptor_lifetime*/, + gss_cred_usage_t * /*cred_usage*/); + struct gssapi_functions { t_gss_delete_sec_context delete_sec_context; t_gss_display_status display_status; t_gss_get_mic get_mic; + t_gss_verify_mic verify_mic; t_gss_import_name import_name; t_gss_init_sec_context init_sec_context; t_gss_release_buffer release_buffer; t_gss_release_cred release_cred; t_gss_release_name release_name; + t_gss_acquire_cred acquire_cred; + t_gss_inquire_cred_by_mech inquire_cred_by_mech; }; #endif /* NO_GSSAPI */ diff --git a/putty.h b/putty.h index 3f2c3f1b..a7cee555 100644 --- a/putty.h +++ b/putty.h @@ -271,6 +271,14 @@ enum { KEX_DHGEX, KEX_RSA, KEX_ECDH, + /* + * KEX_MAX_CONF is a boundary between statically and dynamically configured + * KEXes, without creating a gap in the numbering, allowing easy addition + * of vaues on either side + */ + KEX_MAX_CONF, KEX_DUMMY = KEX_MAX_CONF-1, + /* Kexes from here to KEX_MAX are not explicitly configurable */ + KEX_GSS_SHA1_K5, KEX_MAX }; @@ -796,6 +804,7 @@ void cleanup_exit(int); X(INT, NONE, try_ki_auth) \ X(INT, NONE, try_gssapi_auth) /* attempt gssapi auth */ \ X(INT, NONE, gssapifwd) /* forward tgt via gss */ \ + X(INT, NONE, gssapirekey) /* KEXGSS refresh interval (mins) */ \ X(INT, INT, ssh_gsslist) /* preference order for local GSS libs */ \ X(FILENAME, NONE, ssh_gss_custom) \ X(INT, NONE, ssh_subsys) /* run a subsystem rather than a command */ \ diff --git a/settings.c b/settings.c index 33a0a552..a6b59e28 100644 --- a/settings.c +++ b/settings.c @@ -7,6 +7,11 @@ #include #include "putty.h" #include "storage.h" +#ifndef NO_GSSAPI +#include "sshgssc.h" +#include "sshgss.h" +#endif + /* The cipher order given here is the default order. */ static const struct keyvalwhere ciphernames[] = { @@ -566,9 +571,10 @@ void save_open_settings(void *sesskey, Conf *conf) write_setting_i(sesskey, "GssapiFwd", conf_get_int(conf, CONF_gssapifwd)); write_setting_i(sesskey, "ChangeUsername", conf_get_int(conf, CONF_change_username)); wprefs(sesskey, "Cipher", ciphernames, CIPHER_MAX, conf, CONF_ssh_cipherlist); - wprefs(sesskey, "KEX", kexnames, KEX_MAX, conf, CONF_ssh_kexlist); + wprefs(sesskey, "KEX", kexnames, KEX_MAX_CONF, conf, CONF_ssh_kexlist); wprefs(sesskey, "HostKey", hknames, HK_MAX, conf, CONF_ssh_hklist); write_setting_i(sesskey, "RekeyTime", conf_get_int(conf, CONF_ssh_rekey_time)); + write_setting_i(sesskey, "GssapiRekey", conf_get_int(conf, CONF_gssapirekey)); write_setting_s(sesskey, "RekeyBytes", conf_get_str(conf, CONF_ssh_rekey_data)); write_setting_i(sesskey, "SshNoAuth", conf_get_int(conf, CONF_ssh_no_userauth)); write_setting_i(sesskey, "SshBanner", conf_get_int(conf, CONF_ssh_show_banner)); @@ -910,10 +916,10 @@ void load_open_settings(void *sesskey, Conf *conf) * a server which offered it then choked, but we never got * a server version string or any other reports. */ const char *default_kexes, - *normal_default = "ecdh,dh-gex-sha1,dh-group14-sha1,rsa," - "WARN,dh-group1-sha1", - *bugdhgex2_default = "ecdh,dh-group14-sha1,rsa," - "WARN,dh-group1-sha1,dh-gex-sha1"; + *normal_default = "gss-sha1-krb5,ecdh,dh-gex-sha1," + "dh-group14-sha1,rsa,WARN,dh-group1-sha1", + *bugdhgex2_default = "gss-sha1-krb5,ecdh,dh-group14-sha1," + "rsa,WARN,dh-group1-sha1,dh-gex-sha1"; char *raw; i = 2 - gppi_raw(sesskey, "BugDHGEx2", 0); if (i == FORCE_ON) @@ -940,12 +946,13 @@ void load_open_settings(void *sesskey, Conf *conf) sfree(raw); raw = dupstr(normal_default); } - gprefs_from_str(raw, kexnames, KEX_MAX, conf, CONF_ssh_kexlist); + gprefs_from_str(raw, kexnames, KEX_MAX_CONF, conf, CONF_ssh_kexlist); sfree(raw); } gprefs(sesskey, "HostKey", "ed25519,ecdsa,rsa,dsa,WARN", hknames, HK_MAX, conf, CONF_ssh_hklist); gppi(sesskey, "RekeyTime", 60, conf, CONF_ssh_rekey_time); + gppi(sesskey, "GssapiRekey", 2, conf, CONF_gssapirekey); gpps(sesskey, "RekeyBytes", "1G", conf, CONF_ssh_rekey_data); { /* SSH-2 only by default */ diff --git a/ssh.c b/ssh.c index 15be3175..2ba3a17c 100644 --- a/ssh.c +++ b/ssh.c @@ -17,6 +17,12 @@ #ifndef NO_GSSAPI #include "sshgssc.h" #include "sshgss.h" +#define GSS_DEF_REKEY_MINS 2 /* Default minutes between GSS cache checks */ +#define MIN_CTXT_LIFETIME 5 /* Avoid rekey with short lifetime (seconds) */ +#define GSS_KEX_CAPABLE (1<<0) /* Can do GSS KEX */ +#define GSS_CRED_UPDATED (1<<1) /* Cred updated since previous delegation */ +#define GSS_CTXT_EXPIRES (1<<2) /* Context expires before next timer */ +#define GSS_CTXT_MAYFAIL (1<<3) /* Context may expire during handshake */ #endif #ifndef FALSE @@ -35,6 +41,7 @@ typedef enum { SSH2_PKTCTX_DHGROUP, SSH2_PKTCTX_DHGEX, SSH2_PKTCTX_ECDHKEX, + SSH2_PKTCTX_GSSKEX, SSH2_PKTCTX_RSAKEX } Pkt_KCtx; typedef enum { @@ -274,6 +281,13 @@ static const char *ssh2_pkt_type(Pkt_KCtx pkt_kctx, Pkt_ACtx pkt_actx, translatek(SSH2_MSG_KEXRSA_DONE, SSH2_PKTCTX_RSAKEX); translatek(SSH2_MSG_KEX_ECDH_INIT, SSH2_PKTCTX_ECDHKEX); translatek(SSH2_MSG_KEX_ECDH_REPLY, SSH2_PKTCTX_ECDHKEX); + translatek(SSH2_MSG_KEXGSS_INIT, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_CONTINUE, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_COMPLETE, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_HOSTKEY, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_ERROR, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_GROUPREQ, SSH2_PKTCTX_GSSKEX); + translatek(SSH2_MSG_KEXGSS_GROUP, SSH2_PKTCTX_GSSKEX); translate(SSH2_MSG_USERAUTH_REQUEST); translate(SSH2_MSG_USERAUTH_FAILURE); translate(SSH2_MSG_USERAUTH_SUCCESS); @@ -731,6 +745,11 @@ static int ssh2_pkt_getbool(struct Packet *pkt); static void ssh_pkt_getstring(struct Packet *pkt, char **p, int *length); static void ssh2_timer(void *ctx, unsigned long now); static int ssh2_timer_update(Ssh ssh, unsigned long rekey_time); +#ifndef NO_GSSAPI +static void ssh2_gss_update(Ssh ssh); +static struct Packet *ssh2_gss_authpacket(Ssh ssh, Ssh_gss_ctx gss_ctx, + const char *authtype); +#endif static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, struct Packet *pktin); static void ssh2_msg_unexpected(Ssh ssh, struct Packet *pktin); @@ -971,10 +990,22 @@ struct ssh_tag { #ifndef NO_GSSAPI /* - * GSSAPI libraries for this session. + * GSSAPI libraries for this session. We need them at key exchange + * and userauth time. + * + * And the gss_ctx we setup at initial key exchange will be used + * during gssapi-keyex userauth time as well. */ struct ssh_gss_liblist *gsslibs; + struct ssh_gss_library *gsslib; + int gss_status; + time_t gss_cred_expiry; /* Re-delegate if newer */ + unsigned long gss_ctxt_lifetime; /* Re-delegate when short */ + Ssh_gss_name gss_srv_name; /* Cached for KEXGSS */ + Ssh_gss_ctx gss_ctx; /* Saved for gssapi-keyex */ + tree234 *transient_hostkey_cache; #endif + int gss_kex_used; /* outside ifdef; always FALSE if NO_GSSAPI */ /* * The last list returned from get_specials. @@ -6342,6 +6373,123 @@ static struct kexinit_algorithm *ssh2_kexinit_addalg(struct kexinit_algorithm return NULL; } +#ifndef NO_GSSAPI +/* + * Data structure managing host keys in sessions based on GSSAPI KEX. + * + * In a session we started with a GSSAPI key exchange, the concept of + * 'host key' has completely different lifetime and security semantics + * from the usual ones. Per RFC 4462 section 2.1, we assume that any + * host key delivered to us in the course of a GSSAPI key exchange is + * _solely_ there to use as a transient fallback within the same + * session, if at the time of a subsequent rekey the GSS credentials + * are temporarily invalid and so a non-GSS KEX method has to be used. + * + * In particular, in a GSS-based SSH deployment, host keys may not + * even _be_ persistent identities for the server; it would be + * legitimate for a server to generate a fresh one routinely if it + * wanted to, like SSH-1 server keys. + * + * So, in this mode, we never touch the persistent host key cache at + * all, either to check keys against it _or_ to store keys in it. + * Instead, we maintain an in-memory cache of host keys that have been + * mentioned in GSS key exchanges within this particular session, and + * we permit precisely those host keys in non-GSS rekeys. + */ +struct ssh_transient_hostkey_cache_entry { + const struct ssh_signkey *alg; + unsigned char *pub_blob; + int pub_len; +}; + +static int ssh_transient_hostkey_cache_cmp(void *av, void *bv) +{ + const struct ssh_transient_hostkey_cache_entry + *a = (const struct ssh_transient_hostkey_cache_entry *)av, + *b = (const struct ssh_transient_hostkey_cache_entry *)bv; + return strcmp(a->alg->name, b->alg->name); +} + +static int ssh_transient_hostkey_cache_find(void *av, void *bv) +{ + const struct ssh_signkey *aalg = (const struct ssh_signkey *)av; + const struct ssh_transient_hostkey_cache_entry + *b = (const struct ssh_transient_hostkey_cache_entry *)bv; + return strcmp(aalg->name, b->alg->name); +} + +static void ssh_init_transient_hostkey_store(Ssh ssh) +{ + ssh->transient_hostkey_cache = + newtree234(ssh_transient_hostkey_cache_cmp); +} + +static void ssh_cleanup_transient_hostkey_store(Ssh ssh) +{ + struct ssh_transient_hostkey_cache_entry *ent; + while ((ent = delpos234(ssh->transient_hostkey_cache, 0)) != NULL) { + sfree(ent->pub_blob); + sfree(ent); + } + freetree234(ssh->transient_hostkey_cache); +} + +static void ssh_store_transient_hostkey( + Ssh ssh, const struct ssh_signkey *alg, void *key) +{ + struct ssh_transient_hostkey_cache_entry *ent, *retd; + + if ((ent = find234(ssh->transient_hostkey_cache, (void *)alg, + ssh_transient_hostkey_cache_find)) != NULL) { + sfree(ent->pub_blob); + sfree(ent); + } + + ent = snew(struct ssh_transient_hostkey_cache_entry); + ent->alg = alg; + ent->pub_blob = alg->public_blob(key, &ent->pub_len); + retd = add234(ssh->transient_hostkey_cache, ent); + assert(retd == ent); +} + +static int ssh_verify_transient_hostkey( + Ssh ssh, const struct ssh_signkey *alg, void *key) +{ + struct ssh_transient_hostkey_cache_entry *ent; + int toret = FALSE; + + if ((ent = find234(ssh->transient_hostkey_cache, (void *)alg, + ssh_transient_hostkey_cache_find)) != NULL) { + int this_len; + unsigned char *this_blob = alg->public_blob(key, &this_len); + + if (this_len == ent->pub_len && + !memcmp(this_blob, ent->pub_blob, this_len)) + toret = TRUE; + + sfree(this_blob); + } + + return toret; +} + +static int ssh_have_transient_hostkey(Ssh ssh, const struct ssh_signkey *alg) +{ + struct ssh_transient_hostkey_cache_entry *ent = + find234(ssh->transient_hostkey_cache, (void *)alg, + ssh_transient_hostkey_cache_find); + return ent != NULL; +} + +static int ssh_have_any_transient_hostkey(Ssh ssh) +{ + return count234(ssh->transient_hostkey_cache) > 0; +} + +#endif /* NO_GSSAPI */ + +#define GSS_UPDATE_REKEY_REASON "GSS credentials updated" + /* * Handle the SSH-2 transport layer. */ @@ -6383,6 +6531,9 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, void *eckey; /* for ECDH kex */ unsigned char exchange_hash[SSH2_KEX_MAX_HASH_LEN]; int n_preferred_kex; + int can_gssapi_keyex; + int need_gss_transient_hostkey; + int warned_about_no_gss_transient_hostkey; const struct ssh_kexes *preferred_kex[KEX_MAX]; int n_preferred_hk; int preferred_hk[HK_MAX]; @@ -6397,6 +6548,17 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, int guessok; int ignorepkt; struct kexinit_algorithm kexlists[NKEXLIST][MAXKEXLIST]; +#ifndef NO_GSSAPI + Ssh_gss_buf gss_buf; + Ssh_gss_buf gss_rcvtok, gss_sndtok; + Ssh_gss_stat gss_stat; + Ssh_gss_ctx gss_ctx; + Ssh_gss_buf mic; + int init_token_sent; + int complete_rcvd; + int gss_delegate; + time_t gss_cred_expiry; +#endif }; crState(do_ssh2_transport_state); @@ -6412,6 +6574,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, s->got_session_id = s->activated_authconn = FALSE; s->userauth_succeeded = FALSE; s->pending_compression = FALSE; + s->need_gss_transient_hostkey = FALSE; + s->warned_about_no_gss_transient_hostkey = FALSE; /* * Be prepared to work around the buggy MAC problem. @@ -6422,6 +6586,52 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, s->maclist = macs, s->nmacs = lenof(macs); begin_key_exchange: + +#ifndef NO_GSSAPI + if (s->need_gss_transient_hostkey) { + /* + * This flag indicates a special case in which we must not do + * GSS key exchange even if we could. (See comments below, + * where the flag was set on the previous key exchange.) + */ + s->can_gssapi_keyex = FALSE; + } else { + /* + * We always check if we have GSS creds before we come up with + * the kex algorithm list, otherwise future rekeys will fail + * when creds expire. To make this so, this code section must + * follow the begin_key_exchange label above, otherwise this + * section would execute just once per-connection. + * + * Update GSS state unless the reason we're here is that a + * timer just checked the GSS state and decided that we should + * rekey to update delegated credentials. In that case, the + * state is "fresh". + */ + if (!vin || strcmp(vin, GSS_UPDATE_REKEY_REASON) != 0) + ssh2_gss_update(ssh); + + /* Do GSSAPI KEX when capable */ + s->can_gssapi_keyex = ssh->gss_status & GSS_KEX_CAPABLE; + + /* + * But not when failure is likely. [ GSS implementations may + * attempt (and fail) to use a ticket that is almost expired + * when retrieved from the ccache that actually expires by the + * time the server receives it. ] + * + * Note: The first time always try KEXGSS if we can, failures + * will be very rare, and disabling the initial GSS KEX is + * worse. Some day GSS libraries will ignore cached tickets + * whose lifetime is critically short, and will instead use + * fresh ones. + */ + if (!s->got_session_id && (ssh->gss_status & GSS_CTXT_MAYFAIL) != 0) + s->can_gssapi_keyex = 0; + s->gss_delegate = conf_get_int(ssh->conf, CONF_gssapifwd); + } +#endif + ssh->pkt_kctx = SSH2_PKTCTX_NOKEX; { int i, j, k, warn; @@ -6431,7 +6641,9 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, * Set up the preferred key exchange. (NULL => warn below here) */ s->n_preferred_kex = 0; - for (i = 0; i < KEX_MAX; i++) { + if (s->can_gssapi_keyex) + s->preferred_kex[s->n_preferred_kex++] = &ssh_gssk5_sha1_kex; + for (i = 0; i < KEX_MAX_CONF; i++) { switch (conf_get_int_int(ssh->conf, CONF_ssh_kexlist, i)) { case KEX_DHGEX: s->preferred_kex[s->n_preferred_kex++] = @@ -6590,6 +6802,36 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, alg->u.hk.warn = warn; } } +#ifndef NO_GSSAPI + } else if (ssh->gss_kex_used && !s->need_gss_transient_hostkey) { + /* + * If we've previously done a GSSAPI KEX, then we list + * precisely the algorithms for which a previous GSS key + * exchange has delivered us a host key, because we expect + * one of exactly those keys to be used in any subsequent + * non-GSS-based rekey. + * + * An exception is if this is the key exchange we + * triggered for the purposes of populating that cache - + * in which case the cache will currently be empty, which + * isn't helpful! + */ + warn = FALSE; + for (i = 0; i < s->n_preferred_hk; i++) { + if (s->preferred_hk[i] == HK_WARN) + warn = TRUE; + for (j = 0; j < lenof(hostkey_algs); j++) { + if (hostkey_algs[j].id != s->preferred_hk[i]) + continue; + if (ssh_have_transient_hostkey(ssh, hostkey_algs[j].alg)) { + alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY], + hostkey_algs[j].alg->name); + alg->u.hk.hostkey = hostkey_algs[j].alg; + alg->u.hk.warn = warn; + } + } + } +#endif } else { /* * In subsequent key exchanges, we list only the kex @@ -6604,6 +6846,10 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, alg->u.hk.hostkey = ssh->hostkey; alg->u.hk.warn = FALSE; } + if (s->can_gssapi_keyex) { + alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY], "null"); + alg->u.hk.hostkey = NULL; + } /* List encryption algorithms (client->server then server->client). */ for (k = KEXLIST_CSCIPHER; k <= KEXLIST_SCCIPHER; k++) { warn = FALSE; @@ -6768,6 +7014,15 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, ssh->kex = alg->u.kex.kex; s->warn_kex = alg->u.kex.warn; } else if (i == KEXLIST_HOSTKEY) { + /* + * Ignore an unexpected/inappropriate offer of "null", + * we offer "null" when we're willing to use GSS KEX, + * but it is only acceptable when GSSKEX is actually + * selected. + */ + if (alg->u.hk.hostkey == NULL && + ssh->kex->main_type != KEXTYPE_GSS) + continue; ssh->hostkey = alg->u.hk.hostkey; s->warn_hk = alg->u.hk.warn; } else if (i == KEXLIST_CSCIPHER) { @@ -6798,7 +7053,9 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, crStopV; matched:; - if (i == KEXLIST_HOSTKEY) { + if (i == KEXLIST_HOSTKEY && + !ssh->gss_kex_used && + ssh->kex->main_type != KEXTYPE_GSS) { int j; /* @@ -7209,7 +7466,266 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, } ssh_ecdhkex_freekey(s->eckey); +#ifndef NO_GSSAPI + } else if (ssh->kex->main_type == KEXTYPE_GSS) { + int len; + char *data; + + ssh->pkt_kctx = SSH2_PKTCTX_GSSKEX; + s->init_token_sent = 0; + s->complete_rcvd = 0; + s->hostkeydata = NULL; + s->hostkeylen = 0; + s->hkey = NULL; + s->fingerprint = NULL; + s->keystr = NULL; + + /* + * Work out the number of bits of key we will need from the + * key exchange. We start with the maximum key length of + * either cipher... + * + * This is rote from the KEXTYPE_DH section above. + */ + { + int csbits, scbits; + + csbits = s->cscipher_tobe->real_keybits; + scbits = s->sccipher_tobe->real_keybits; + s->nbits = (csbits > scbits ? csbits : scbits); + } + /* The keys only have hlen-bit entropy, since they're based on + * a hash. So cap the key size at hlen bits. */ + if (s->nbits > ssh->kex->hash->hlen * 8) + s->nbits = ssh->kex->hash->hlen * 8; + + if (dh_is_gex(ssh->kex)) { + /* + * Work out how big a DH group we will need to allow that + * much data. + */ + s->pbits = 512 << ((s->nbits - 1) / 64); + logeventf(ssh, "Doing GSSAPI (with Kerberos V5) Diffie-Hellman " + "group exchange, with minimum %d bits", s->pbits); + s->pktout = ssh2_pkt_init(SSH2_MSG_KEXGSS_GROUPREQ); + ssh2_pkt_adduint32(s->pktout, s->pbits); /* min */ + ssh2_pkt_adduint32(s->pktout, s->pbits); /* preferred */ + ssh2_pkt_adduint32(s->pktout, s->pbits * 2); /* max */ + ssh2_pkt_send_noqueue(ssh, s->pktout); + + crWaitUntilV(pktin); + if (pktin->type != SSH2_MSG_KEXGSS_GROUP) { + bombout(("expected key exchange group packet from server")); + crStopV; + } + s->p = ssh2_pkt_getmp(pktin); + s->g = ssh2_pkt_getmp(pktin); + if (!s->p || !s->g) { + bombout(("unable to read mp-ints from incoming group packet")); + crStopV; + } + ssh->kex_ctx = dh_setup_gex(s->p, s->g); + } else { + ssh->kex_ctx = dh_setup_group(ssh->kex); + logeventf(ssh, "Using GSSAPI (with Kerberos V5) Diffie-Hellman with standard group \"%s\"", + ssh->kex->groupname); + } + + logeventf(ssh, "Doing GSSAPI (with Kerberos V5) Diffie-Hellman key exchange with hash %s", + ssh->kex->hash->text_name); + /* Now generate e for Diffie-Hellman. */ + set_busy_status(ssh->frontend, BUSY_CPU); /* this can take a while */ + s->e = dh_create_e(ssh->kex_ctx, s->nbits * 2); + + if (ssh->gsslib->gsslogmsg) + logevent(ssh->gsslib->gsslogmsg); + + /* initial tokens are empty */ + SSH_GSS_CLEAR_BUF(&s->gss_rcvtok); + SSH_GSS_CLEAR_BUF(&s->gss_sndtok); + SSH_GSS_CLEAR_BUF(&s->mic); + s->gss_stat = ssh->gsslib->acquire_cred(ssh->gsslib, &s->gss_ctx, + &s->gss_cred_expiry); + if (s->gss_stat != SSH_GSS_OK) { + bombout(("GSSAPI key exchange failed to initialize")); + crStopV; + } + + /* now enter the loop */ + assert(ssh->gss_srv_name); + do { + /* + * When acquire_cred yields no useful expiration, go with the + * service ticket expiration. + */ + s->gss_stat = ssh->gsslib->init_sec_context + (ssh->gsslib, + &s->gss_ctx, + ssh->gss_srv_name, + s->gss_delegate, + &s->gss_rcvtok, + &s->gss_sndtok, + (s->gss_cred_expiry == GSS_NO_EXPIRATION ? + &s->gss_cred_expiry : NULL), + NULL); + SSH_GSS_CLEAR_BUF(&s->gss_rcvtok); + + if (s->gss_stat == SSH_GSS_S_COMPLETE && s->complete_rcvd) + break; /* MIC is verified after the loop */ + + if (s->gss_stat != SSH_GSS_S_COMPLETE && + s->gss_stat != SSH_GSS_S_CONTINUE_NEEDED) { + if (ssh->gsslib->display_status(ssh->gsslib, s->gss_ctx, + &s->gss_buf) == SSH_GSS_OK) { + bombout(("GSSAPI key exchange failed to initialize" + " context: %s", (char *)s->gss_buf.value)); + sfree(s->gss_buf.value); + crStopV; + } else { + bombout(("GSSAPI key exchange failed to initialize" + " context")); + crStopV; + } + } + assert(s->gss_stat == SSH_GSS_S_COMPLETE || + s->gss_stat == SSH_GSS_S_CONTINUE_NEEDED); + + if (!s->init_token_sent) { + s->init_token_sent = 1; + s->pktout = ssh2_pkt_init(SSH2_MSG_KEXGSS_INIT); + if (s->gss_sndtok.length == 0) { + bombout(("GSSAPI key exchange failed:" + " no initial context token")); + crStopV; + } + ssh_pkt_addstring_start(s->pktout); + ssh_pkt_addstring_data(s->pktout, + s->gss_sndtok.value, + s->gss_sndtok.length); + ssh2_pkt_addmp(s->pktout, s->e); + ssh2_pkt_send_noqueue(ssh, s->pktout); + ssh->gsslib->free_tok(ssh->gsslib, &s->gss_sndtok); + logevent("GSSAPI key exchange initialised"); + } else if (s->gss_sndtok.length != 0) { + s->pktout = ssh2_pkt_init(SSH2_MSG_KEXGSS_CONTINUE); + ssh_pkt_addstring_start(s->pktout); + ssh_pkt_addstring_data(s->pktout, + s->gss_sndtok.value, + s->gss_sndtok.length); + ssh2_pkt_send_noqueue(ssh, s->pktout); + ssh->gsslib->free_tok(ssh->gsslib, &s->gss_sndtok); + } + + if (s->gss_stat == SSH_GSS_S_COMPLETE && s->complete_rcvd) + break; + + wait_for_gss_token: + crWaitUntilV(pktin); + switch (pktin->type) { + case SSH2_MSG_KEXGSS_CONTINUE: + ssh_pkt_getstring(pktin, &data, &len); + s->gss_rcvtok.value = data; + s->gss_rcvtok.length = len; + continue; + case SSH2_MSG_KEXGSS_COMPLETE: + s->complete_rcvd = 1; + s->f = ssh2_pkt_getmp(pktin); + ssh_pkt_getstring(pktin, &data, &len); + s->mic.value = data; + s->mic.length = len; + /* Save expiration time of cred when delegating */ + if (s->gss_delegate && s->gss_cred_expiry != GSS_NO_EXPIRATION) + ssh->gss_cred_expiry = s->gss_cred_expiry; + /* If there's a final token we loop to consume it */ + if (ssh2_pkt_getbool(pktin)) { + ssh_pkt_getstring(pktin, &data, &len); + s->gss_rcvtok.value = data; + s->gss_rcvtok.length = len; + continue; + } + break; + case SSH2_MSG_KEXGSS_HOSTKEY: + ssh_pkt_getstring(pktin, &s->hostkeydata, &s->hostkeylen); + if (ssh->hostkey) { + s->hkey = ssh->hostkey->newkey(ssh->hostkey, + s->hostkeydata, + s->hostkeylen); + hash_string(ssh->kex->hash, ssh->exhash, + s->hostkeydata, s->hostkeylen); + } + /* + * Can't loop as we have no token to pass to + * init_sec_context. + */ + goto wait_for_gss_token; + case SSH2_MSG_KEXGSS_ERROR: + /* + * We have no use for the server's major and minor + * status. The minor status is really only + * meaningful to the server, and with luck the major + * status means something to us (but not really all + * that much). The string is more meaningful, and + * hopefully the server sends any error tokens, as + * that will produce the most useful information for + * us. + */ + ssh_pkt_getuint32(pktin); /* server's major status */ + ssh_pkt_getuint32(pktin); /* server's minor status */ + ssh_pkt_getstring(pktin, &data, &len); + logeventf(ssh, "GSSAPI key exchange failed; " + "server's message: %.*s", len, data); + /* Language tag, but we have no use for it */ + ssh_pkt_getstring(pktin, &data, &len); + /* + * Wait for an error token, if there is one, or the + * server's disconnect. The error token, if there + * is one, must follow the SSH2_MSG_KEXGSS_ERROR + * message, per the RFC. + */ + goto wait_for_gss_token; + default: + bombout(("unexpected message type during gss kex")); + crStopV; + break; + } + } while (s->gss_rcvtok.length || + s->gss_stat == SSH_GSS_S_CONTINUE_NEEDED || + !s->complete_rcvd); + + s->K = dh_find_K(ssh->kex_ctx, s->f); + + /* We assume everything from now on will be quick, and it might + * involve user interaction. */ + set_busy_status(ssh->frontend, BUSY_NOT); + + if (!s->hkey) + hash_string(ssh->kex->hash, ssh->exhash, NULL, 0); + if (dh_is_gex(ssh->kex)) { + /* min, preferred, max */ + hash_uint32(ssh->kex->hash, ssh->exhash, s->pbits); + hash_uint32(ssh->kex->hash, ssh->exhash, s->pbits); + hash_uint32(ssh->kex->hash, ssh->exhash, s->pbits * 2); + + hash_mpint(ssh->kex->hash, ssh->exhash, s->p); + hash_mpint(ssh->kex->hash, ssh->exhash, s->g); + } + hash_mpint(ssh->kex->hash, ssh->exhash, s->e); + hash_mpint(ssh->kex->hash, ssh->exhash, s->f); + + /* + * MIC verification is done below, after we compute the hash + * used as the MIC input. + */ + + dh_cleanup(ssh->kex_ctx); + freebn(s->f); + if (dh_is_gex(ssh->kex)) { + freebn(s->g); + freebn(s->p); + } +#endif } else { + assert(ssh->kex->main_type == KEXTYPE_RSA); logeventf(ssh, "Doing RSA key exchange with hash %s", ssh->kex->hash->text_name); ssh->pkt_kctx = SSH2_PKTCTX_RSAKEX; @@ -7328,6 +7844,50 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, assert(ssh->kex->hash->hlen <= sizeof(s->exchange_hash)); ssh->kex->hash->final(ssh->exhash, s->exchange_hash); +#ifndef NO_GSSAPI + if (ssh->kex->main_type == KEXTYPE_GSS) { + Ssh_gss_buf gss_buf; + SSH_GSS_CLEAR_BUF(&s->gss_buf); + + gss_buf.value = s->exchange_hash; + gss_buf.length = ssh->kex->hash->hlen; + s->gss_stat = ssh->gsslib->verify_mic(ssh->gsslib, s->gss_ctx, &gss_buf, &s->mic); + if (s->gss_stat != SSH_GSS_OK) { + if (ssh->gsslib->display_status(ssh->gsslib, s->gss_ctx, + &s->gss_buf) == SSH_GSS_OK) { + bombout(("GSSAPI Key Exchange MIC was not valid: %s", + (char *)s->gss_buf.value)); + sfree(s->gss_buf.value); + } else { + bombout(("GSSAPI Key Exchange MIC was not valid")); + } + crStopV; + } + + ssh->gss_kex_used = TRUE; + + /*- + * If this the first KEX, save the GSS context for "gssapi-keyex" + * authentication. + * + * http://tools.ietf.org/html/rfc4462#section-4 + * + * This method may be used only if the initial key exchange was + * performed using a GSS-API-based key exchange method defined in + * accordance with Section 2. The GSS-API context used with this + * method is always that established during an initial GSS-API-based + * key exchange. Any context established during key exchange for the + * purpose of rekeying MUST NOT be used with this method. + */ + if (!s->got_session_id) { + ssh->gss_ctx = s->gss_ctx; + } else { + ssh->gsslib->release_cred(ssh->gsslib, &s->gss_ctx); + } + logeventf(ssh, "GSSAPI Key Exchange complete!"); + } +#endif + ssh->kex_ctx = NULL; #if 0 @@ -7335,21 +7895,118 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, dmemdump(s->exchange_hash, ssh->kex->hash->hlen); #endif - if (!s->hkey) { - bombout(("Server's host key is invalid")); - crStopV; - } + /* In GSS keyex there's no hostkey signature to verify */ + if (ssh->kex->main_type != KEXTYPE_GSS) { + if (!s->hkey) { + bombout(("Server's host key is invalid")); + crStopV; + } - if (!ssh->hostkey->verifysig(s->hkey, s->sigdata, s->siglen, - (char *)s->exchange_hash, - ssh->kex->hash->hlen)) { + if (!ssh->hostkey->verifysig(s->hkey, s->sigdata, s->siglen, + (char *)s->exchange_hash, + ssh->kex->hash->hlen)) { #ifndef FUZZING - bombout(("Server's host key did not match the signature supplied")); - crStopV; + bombout(("Server's host key did not match the signature " + "supplied")); + crStopV; #endif + } } - s->keystr = ssh->hostkey->fmtkey(s->hkey); + s->keystr = (ssh->hostkey && s->hkey ? + ssh->hostkey->fmtkey(s->hkey) : NULL); +#ifndef NO_GSSAPI + if (ssh->gss_kex_used) { + /* + * In a GSS-based session, check the host key (if any) against + * the transient host key cache. See comment above, at the + * definition of ssh_transient_hostkey_cache_entry. + */ + if (ssh->kex->main_type == KEXTYPE_GSS) { + + /* + * We've just done a GSS key exchange. If it gave us a + * host key, store it. + */ + if (s->hkey) { + s->fingerprint = ssh2_fingerprint(ssh->hostkey, s->hkey); + logevent("GSS kex provided fallback host key:"); + logevent(s->fingerprint); + sfree(s->fingerprint); + s->fingerprint = NULL; + ssh_store_transient_hostkey(ssh, ssh->hostkey, s->hkey); + } else if (!ssh_have_any_transient_hostkey(ssh)) { + /* + * But if it didn't, then we currently have no + * fallback host key to use in subsequent non-GSS + * rekeys. So we should immediately trigger a non-GSS + * rekey of our own, to set one up, before the session + * keys have been used for anything else. + * + * This is similar to the cross-certification done at + * user request in the permanent host key cache, but + * here we do it automatically, once, at session + * startup, and only add the key to the transient + * cache. + */ + if (ssh->hostkey) { + s->need_gss_transient_hostkey = TRUE; + } else { + /* + * If we negotiated the "null" host key algorithm + * in the key exchange, that's an indication that + * no host key at all is available from the server + * (both because we listed "null" last, and + * because RFC 4462 section 5 says that a server + * MUST NOT offer "null" as a host key algorithm + * unless that is the only algorithm it provides + * at all). + * + * In that case we actually _can't_ perform a + * non-GSSAPI key exchange, so it's pointless to + * attempt one proactively. This is also likely to + * cause trouble later if a rekey is required at a + * moment whne GSS credentials are not available, + * but someone setting up a server in this + * configuration presumably accepts that as a + * consequence. + */ + if (!s->warned_about_no_gss_transient_hostkey) { + logevent("No fallback host key available"); + s->warned_about_no_gss_transient_hostkey = TRUE; + } + } + } + } else { + /* + * We've just done a fallback key exchange, so make + * sure the host key it used is in the cache of keys + * we previously received in GSS kexes. + * + * An exception is if this was the non-GSS key exchange we + * triggered on purpose to populate the transient cache. + */ + assert(s->hkey); /* only KEXTYPE_GSS lets this be null */ + s->fingerprint = ssh2_fingerprint(ssh->hostkey, s->hkey); + + if (s->need_gss_transient_hostkey) { + logevent("Post-GSS rekey provided fallback host key:"); + logevent(s->fingerprint); + ssh_store_transient_hostkey(ssh, ssh->hostkey, s->hkey); + s->need_gss_transient_hostkey = FALSE; + } else if (!ssh_verify_transient_hostkey( + ssh, ssh->hostkey, s->hkey)) { + logevent("Non-GSS rekey after initial GSS kex " + "used host key:"); + logevent(s->fingerprint); + bombout(("Host key was not previously sent via GSS kex")); + } + + sfree(s->fingerprint); + s->fingerprint = NULL; + } + } else +#endif /* NO_GSSAPI */ if (!s->got_session_id) { /* * Make a note of any other host key formats that are available. @@ -7434,6 +8091,7 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, * subsequent rekeys. */ ssh->hostkey_str = s->keystr; + s->keystr = NULL; } else if (ssh->cross_certifying) { s->fingerprint = ssh2_fingerprint(ssh->hostkey, s->hkey); logevent("Storing additional host key for this host:"); @@ -7447,6 +8105,7 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, * re-checking in future normal rekeys. */ ssh->hostkey_str = s->keystr; + s->keystr = NULL; } else { /* * In a rekey, we never present an interactive host key @@ -7460,9 +8119,12 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, crStopV; #endif } - sfree(s->keystr); } - ssh->hostkey->freekey(s->hkey); + sfree(s->keystr); + if (s->hkey) { + ssh->hostkey->freekey(s->hkey); + s->hkey = NULL; + } /* * The exchange hash from the very first key exchange is also @@ -7689,29 +8351,45 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen, } else { if (inlen == -2) { /* - * authconn has seen a USERAUTH_SUCCEEDED. Time to enable - * delayed compression, if it's available. + * authconn has seen a USERAUTH_SUCCEEDED. For a couple of + * reasons, this may be the moment to do an immediate + * rekey with different parameters. * + * One is to turn on delayed compression. We do this by a + * rekey to work around a protocol design bug: * draft-miller-secsh-compression-delayed-00 says that you - * negotiate delayed compression in the first key exchange, and - * both sides start compressing when the server has sent - * USERAUTH_SUCCESS. This has a race condition -- the server - * can't know when the client has seen it, and thus which incoming - * packets it should treat as compressed. + * negotiate delayed compression in the first key + * exchange, and both sides start compressing when the + * server has sent USERAUTH_SUCCESS. This has a race + * condition -- the server can't know when the client has + * seen it, and thus which incoming packets it should + * treat as compressed. * - * Instead, we do the initial key exchange without offering the - * delayed methods, but note if the server offers them; when we - * get here, if a delayed method was available that was higher - * on our list than what we got, we initiate a rekey in which we - * _do_ list the delayed methods (and hopefully get it as a - * result). Subsequent rekeys will do the same. + * Instead, we do the initial key exchange without + * offering the delayed methods, but note if the server + * offers them; when we get here, if a delayed method was + * available that was higher on our list than what we got, + * we initiate a rekey in which we _do_ list the delayed + * methods (and hopefully get it as a result). Subsequent + * rekeys will do the same. + * + * Another reason for a rekey at this point is if we've + * done a GSS key exchange and don't have anything in our + * transient hostkey cache, in which case we should make + * an attempt to populate the cache now. */ assert(!s->userauth_succeeded); /* should only happen once */ s->userauth_succeeded = TRUE; - if (!s->pending_compression) + if (s->pending_compression) { + in = (void *)"enabling delayed compression"; + } else if (s->need_gss_transient_hostkey) { + in = (void *)"populating transient host key cache"; + } else { /* Can't see any point rekeying. */ goto wait_for_rekey; /* this is utterly horrid */ + } /* else fall through to rekey... */ + s->pending_compression = FALSE; } /* @@ -9246,7 +9924,10 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, int tried_pubkey_config, done_agent; #ifndef NO_GSSAPI int can_gssapi; + int can_gssapi_keyex_auth; int tried_gssapi; + int tried_gssapi_keyex_auth; + time_t gss_cred_expiry; #endif int kbd_inter_refused; int we_are_in, userauth_success; @@ -9271,11 +9952,9 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, struct Packet *pktout; Filename *keyfile; #ifndef NO_GSSAPI - struct ssh_gss_library *gsslib; Ssh_gss_ctx gss_ctx; Ssh_gss_buf gss_buf; Ssh_gss_buf gss_rcvtok, gss_sndtok; - Ssh_gss_name gss_srv_name; Ssh_gss_stat gss_stat; #endif }; @@ -9310,6 +9989,7 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, s->agent_response = NULL; #ifndef NO_GSSAPI s->tried_gssapi = FALSE; + s->tried_gssapi_keyex_auth = FALSE; #endif if (!ssh->bare_connection) { @@ -9744,25 +10424,43 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, s->can_keyb_inter = conf_get_int(ssh->conf, CONF_try_ki_auth) && in_commasep_string("keyboard-interactive", methods, methlen); #ifndef NO_GSSAPI - if (conf_get_int(ssh->conf, CONF_try_gssapi_auth) && - in_commasep_string("gssapi-with-mic", methods, methlen)) { - /* Try loading the GSS libraries and see if we - * have any. */ - if (!ssh->gsslibs) - ssh->gsslibs = ssh_gss_setup(ssh->conf); - s->can_gssapi = (ssh->gsslibs->nlibraries > 0); - } else { - /* No point in even bothering to try to load the - * GSS libraries, if the user configuration and - * server aren't both prepared to attempt GSSAPI - * auth in the first place. */ - s->can_gssapi = FALSE; - } + s->can_gssapi = + conf_get_int(ssh->conf, CONF_try_gssapi_auth) && + in_commasep_string("gssapi-with-mic", methods, methlen) && + ssh->gsslibs->nlibraries > 0; + s->can_gssapi_keyex_auth = + conf_get_int(ssh->conf, CONF_try_gssapi_auth) && + in_commasep_string("gssapi-keyex", methods, methlen) && + ssh->gsslibs->nlibraries > 0 && + ssh->gss_ctx; #endif } ssh->pkt_actx = SSH2_PKTCTX_NOAUTH; +#ifndef NO_GSSAPI + if (s->can_gssapi_keyex_auth && !s->tried_gssapi_keyex_auth) { + + /* gssapi-keyex authentication */ + + s->type = AUTH_TYPE_GSSAPI; + s->tried_gssapi_keyex_auth = TRUE; + ssh->pkt_actx = SSH2_PKTCTX_GSSAPI; + + if (ssh->gsslib->gsslogmsg) + logevent(ssh->gsslib->gsslogmsg); + + logeventf(ssh, "Trying gssapi-keyex..."); + s->pktout = + ssh2_gss_authpacket(ssh, ssh->gss_ctx, "gssapi-keyex"); + ssh2_pkt_send(ssh, s->pktout); + ssh->gsslib->release_cred(ssh->gsslib, &ssh->gss_ctx); + ssh->gss_ctx = NULL; + + continue; + } else +#endif /* NO_GSSAPI */ + if (s->can_pubkey && !s->done_agent && s->nkeys) { /* @@ -10096,47 +10794,21 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, #ifndef NO_GSSAPI } else if (s->can_gssapi && !s->tried_gssapi) { - /* GSSAPI Authentication */ + /* gssapi-with-mic authentication */ - int micoffset, len; + int len; char *data; - Ssh_gss_buf mic; + s->type = AUTH_TYPE_GSSAPI; s->tried_gssapi = TRUE; s->gotit = TRUE; ssh->pkt_actx = SSH2_PKTCTX_GSSAPI; - /* - * Pick the highest GSS library on the preference - * list. - */ - { - int i, j; - s->gsslib = NULL; - for (i = 0; i < ngsslibs; i++) { - int want_id = conf_get_int_int(ssh->conf, - CONF_ssh_gsslist, i); - for (j = 0; j < ssh->gsslibs->nlibraries; j++) - if (ssh->gsslibs->libraries[j].id == want_id) { - s->gsslib = &ssh->gsslibs->libraries[j]; - goto got_gsslib; /* double break */ - } - } - got_gsslib: - /* - * We always expect to have found something in - * the above loop: we only came here if there - * was at least one viable GSS library, and the - * preference list should always mention - * everything and only change the order. - */ - assert(s->gsslib); - } - - if (s->gsslib->gsslogmsg) - logevent(s->gsslib->gsslogmsg); + if (ssh->gsslib->gsslogmsg) + logevent(ssh->gsslib->gsslogmsg); /* Sending USERAUTH_REQUEST with "gssapi-with-mic" method */ + logeventf(ssh, "Trying gssapi-with-mic..."); s->pktout = ssh2_pkt_init(SSH2_MSG_USERAUTH_REQUEST); ssh2_pkt_addstring(s->pktout, ssh->username); ssh2_pkt_addstring(s->pktout, "ssh-connection"); @@ -10144,7 +10816,7 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, logevent("Attempting GSSAPI authentication"); /* add mechanism info */ - s->gsslib->indicate_mech(s->gsslib, &s->gss_buf); + ssh->gsslib->indicate_mech(ssh->gsslib, &s->gss_buf); /* number of GSSAPI mechanisms */ ssh2_pkt_adduint32(s->pktout,1); @@ -10179,24 +10851,26 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, continue; } - /* now start running */ - s->gss_stat = s->gsslib->import_name(s->gsslib, - ssh->fullhostname, - &s->gss_srv_name); - if (s->gss_stat != SSH_GSS_OK) { - if (s->gss_stat == SSH_GSS_BAD_HOST_NAME) - logevent("GSSAPI import name failed - Bad service name"); - else - logevent("GSSAPI import name failed"); - continue; + /* Import server name if not cached from KEX */ + if (ssh->gss_srv_name == GSS_C_NO_NAME) { + s->gss_stat = ssh->gsslib->import_name(ssh->gsslib, + ssh->fullhostname, + &ssh->gss_srv_name); + if (s->gss_stat != SSH_GSS_OK) { + if (s->gss_stat == SSH_GSS_BAD_HOST_NAME) + logevent("GSSAPI import name failed -" + " Bad service name"); + else + logevent("GSSAPI import name failed"); + continue; + } } - /* fetch TGT into GSS engine */ - s->gss_stat = s->gsslib->acquire_cred(s->gsslib, &s->gss_ctx); - + /* Allocate our gss_ctx */ + s->gss_stat = ssh->gsslib->acquire_cred(ssh->gsslib, + &s->gss_ctx, NULL); if (s->gss_stat != SSH_GSS_OK) { logevent("GSSAPI authentication failed to get credentials"); - s->gsslib->release_name(s->gsslib, &s->gss_srv_name); continue; } @@ -10206,20 +10880,26 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, /* now enter the loop */ do { - s->gss_stat = s->gsslib->init_sec_context - (s->gsslib, + /* + * When acquire_cred yields no useful expiration, go with + * the service ticket expiration. + */ + s->gss_stat = ssh->gsslib->init_sec_context + (ssh->gsslib, &s->gss_ctx, - s->gss_srv_name, + ssh->gss_srv_name, conf_get_int(ssh->conf, CONF_gssapifwd), &s->gss_rcvtok, - &s->gss_sndtok); + &s->gss_sndtok, + NULL, + NULL); if (s->gss_stat!=SSH_GSS_S_COMPLETE && s->gss_stat!=SSH_GSS_S_CONTINUE_NEEDED) { logevent("GSSAPI authentication initialisation failed"); - if (s->gsslib->display_status(s->gsslib, s->gss_ctx, - &s->gss_buf) == SSH_GSS_OK) { + if (ssh->gsslib->display_status(ssh->gsslib, + s->gss_ctx, &s->gss_buf) == SSH_GSS_OK) { logevent(s->gss_buf.value); sfree(s->gss_buf.value); } @@ -10228,21 +10908,25 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, } logevent("GSSAPI authentication initialised"); - /* Client and server now exchange tokens until GSSAPI - * no longer says CONTINUE_NEEDED */ - + /* + * Client and server now exchange tokens until GSSAPI + * no longer says CONTINUE_NEEDED + */ if (s->gss_sndtok.length != 0) { - s->pktout = ssh2_pkt_init(SSH2_MSG_USERAUTH_GSSAPI_TOKEN); + s->pktout = + ssh2_pkt_init(SSH2_MSG_USERAUTH_GSSAPI_TOKEN); ssh_pkt_addstring_start(s->pktout); - ssh_pkt_addstring_data(s->pktout,s->gss_sndtok.value,s->gss_sndtok.length); + ssh_pkt_addstring_data(s->pktout, s->gss_sndtok.value, + s->gss_sndtok.length); ssh2_pkt_send(ssh, s->pktout); - s->gsslib->free_tok(s->gsslib, &s->gss_sndtok); + ssh->gsslib->free_tok(ssh->gsslib, &s->gss_sndtok); } if (s->gss_stat == SSH_GSS_S_CONTINUE_NEEDED) { crWaitUntilV(pktin); if (pktin->type != SSH2_MSG_USERAUTH_GSSAPI_TOKEN) { - logevent("GSSAPI authentication - bad server response"); + logevent("GSSAPI authentication -" + " bad server response"); s->gss_stat = SSH_GSS_FAILURE; break; } @@ -10253,37 +10937,20 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, } while (s-> gss_stat == SSH_GSS_S_CONTINUE_NEEDED); if (s->gss_stat != SSH_GSS_OK) { - s->gsslib->release_name(s->gsslib, &s->gss_srv_name); - s->gsslib->release_cred(s->gsslib, &s->gss_ctx); + ssh->gsslib->release_cred(ssh->gsslib, &s->gss_ctx); continue; } logevent("GSSAPI authentication loop finished OK"); /* Now send the MIC */ - s->pktout = ssh2_pkt_init(0); - micoffset = s->pktout->length; - ssh_pkt_addstring_start(s->pktout); - ssh_pkt_addstring_data(s->pktout, (char *)ssh->v2_session_id, ssh->v2_session_id_len); - ssh_pkt_addbyte(s->pktout, SSH2_MSG_USERAUTH_REQUEST); - ssh_pkt_addstring(s->pktout, ssh->username); - ssh_pkt_addstring(s->pktout, "ssh-connection"); - ssh_pkt_addstring(s->pktout, "gssapi-with-mic"); - - s->gss_buf.value = (char *)s->pktout->data + micoffset; - s->gss_buf.length = s->pktout->length - micoffset; - - s->gsslib->get_mic(s->gsslib, s->gss_ctx, &s->gss_buf, &mic); - s->pktout = ssh2_pkt_init(SSH2_MSG_USERAUTH_GSSAPI_MIC); - ssh_pkt_addstring_start(s->pktout); - ssh_pkt_addstring_data(s->pktout, mic.value, mic.length); + s->pktout = + ssh2_gss_authpacket(ssh, s->gss_ctx, "gssapi-with-mic"); ssh2_pkt_send(ssh, s->pktout); - s->gsslib->free_mic(s->gsslib, &mic); s->gotit = FALSE; - s->gsslib->release_name(s->gsslib, &s->gss_srv_name); - s->gsslib->release_cred(s->gsslib, &s->gss_ctx); + ssh->gsslib->release_cred(ssh->gsslib, &s->gss_ctx); continue; #endif } else if (s->can_keyb_inter && !s->kbd_inter_refused) { @@ -10699,16 +11366,17 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen, if (s->userauth_success && !ssh->bare_connection) { /* - * We've just received USERAUTH_SUCCESS, and we haven't sent any - * packets since. Signal the transport layer to consider enacting - * delayed compression. + * We've just received USERAUTH_SUCCESS, and we haven't sent + * any packets since. Signal the transport layer to consider + * doing an immediate rekey, if it has any reason to want to. * - * (Relying on we_are_in is not sufficient, as - * draft-miller-secsh-compression-delayed is quite clear that it - * triggers on USERAUTH_SUCCESS specifically, and we_are_in can - * become set for other reasons.) + * (Relying on we_are_in is not sufficient. One of the reasons + * to do a post-userauth rekey is OpenSSH delayed compression; + * draft-miller-secsh-compression-delayed is quite clear that + * that triggers on USERAUTH_SUCCESS specifically, and + * we_are_in can become set for other reasons.) */ - do_ssh2_transport(ssh, "enabling delayed compression", -2, NULL); + do_ssh2_transport(ssh, NULL, -2, NULL); } ssh->channels = newtree234(ssh_channelcmp); @@ -11039,6 +11707,34 @@ static void ssh2_protocol_setup(Ssh ssh) { int i; +#ifndef NO_GSSAPI + /* Load and pick the highest GSS library on the preference list. */ + if (!ssh->gsslibs) + ssh->gsslibs = ssh_gss_setup(ssh->conf); + ssh->gsslib = NULL; + if (ssh->gsslibs->nlibraries > 0) { + int i, j; + for (i = 0; i < ngsslibs; i++) { + int want_id = conf_get_int_int(ssh->conf, + CONF_ssh_gsslist, i); + for (j = 0; j < ssh->gsslibs->nlibraries; j++) + if (ssh->gsslibs->libraries[j].id == want_id) { + ssh->gsslib = &ssh->gsslibs->libraries[j]; + goto got_gsslib; /* double break */ + } + } + got_gsslib: + /* + * We always expect to have found something in + * the above loop: we only came here if there + * was at least one viable GSS library, and the + * preference list should always mention + * everything and only change the order. + */ + assert(ssh->gsslib); + } +#endif + /* * Most messages cause SSH2_MSG_UNIMPLEMENTED. */ @@ -11062,6 +11758,7 @@ static void ssh2_protocol_setup(Ssh ssh) /* ssh->packet_dispatch[SSH2_MSG_KEX_DH_GEX_GROUP] = ssh2_msg_transport; duplicate case value */ ssh->packet_dispatch[SSH2_MSG_KEX_DH_GEX_INIT] = ssh2_msg_transport; ssh->packet_dispatch[SSH2_MSG_KEX_DH_GEX_REPLY] = ssh2_msg_transport; + ssh->packet_dispatch[SSH2_MSG_KEXGSS_GROUP] = ssh2_msg_transport; ssh->packet_dispatch[SSH2_MSG_USERAUTH_REQUEST] = ssh2_msg_unexpected; ssh->packet_dispatch[SSH2_MSG_USERAUTH_FAILURE] = ssh2_msg_unexpected; ssh->packet_dispatch[SSH2_MSG_USERAUTH_SUCCESS] = ssh2_msg_unexpected; @@ -11135,6 +11832,161 @@ static void ssh2_bare_connection_protocol_setup(Ssh ssh) ssh->packet_dispatch[SSH2_MSG_DEBUG] = ssh2_msg_debug; } +#ifndef NO_GSSAPI +static struct Packet *ssh2_gss_authpacket(Ssh ssh, Ssh_gss_ctx gss_ctx, + const char *authtype) +{ + struct Packet *p = ssh2_pkt_init(0); + int micoffset = p->length; + Ssh_gss_buf buf; + Ssh_gss_buf mic; + + /* + * The mic is computed over the session id + intended packet, so we + * build an artificial packet with a prepended session id. + */ + ssh_pkt_addstring_start(p); + ssh_pkt_addstring_data(p, (char *)ssh->v2_session_id, + ssh->v2_session_id_len); + ssh_pkt_addbyte(p, SSH2_MSG_USERAUTH_REQUEST); + ssh_pkt_addstring(p, ssh->username); + ssh_pkt_addstring(p, "ssh-connection"); + ssh_pkt_addstring(p, authtype); + + /* Compute the mic */ + buf.value = (char *)p->data + micoffset; + buf.length = p->length - micoffset; + ssh->gsslib->get_mic(ssh->gsslib, gss_ctx, &buf, &mic); + ssh_free_packet(p); + + /* Now we can build the real packet */ + if (strcmp(authtype, "gssapi-with-mic") == 0) { + p = ssh2_pkt_init(SSH2_MSG_USERAUTH_GSSAPI_MIC); + } else { + p = ssh2_pkt_init(SSH2_MSG_USERAUTH_REQUEST); + ssh2_pkt_addstring(p, ssh->username); + ssh2_pkt_addstring(p, "ssh-connection"); + ssh2_pkt_addstring(p, authtype); + } + ssh_pkt_addstring_start(p); + ssh_pkt_addstring_data(p, (char *)mic.value, mic.length); + + return p; +} + +/* + * This is called at the beginning of each SSH rekey to determine whether we are + * GSS capable, and if we did GSS key exchange, and are delegating credentials, + * it is also called periodically to determine whether we should rekey in order + * to delegate (more) fresh credentials. This is called "credential cascading". + * + * On Windows, with SSPI, we may not get the credential expiration, as Windows + * automatically renews from cached passwords, so the credential effectively + * never expires. Since we still want to cascade when the local TGT is updated, + * we use the expiration of a newly obtained context as a proxy for the + * expiration of the TGT. + */ +static void ssh2_gss_update(Ssh ssh) +{ + int gss_stat; + time_t gss_cred_expiry; + unsigned long mins; + Ssh_gss_buf gss_sndtok; + Ssh_gss_buf gss_rcvtok; + Ssh_gss_ctx gss_ctx; + + ssh->gss_status = 0; + + /* + * Nothing to do if no GSSAPI libraries are configured or GSSAPI auth is not + * enabled. + */ + if (ssh->gsslibs->nlibraries == 0) + return; + if (!conf_get_int(ssh->conf, CONF_try_gssapi_auth)) + return; + + /* Import server name and cache it */ + if (ssh->gss_srv_name == GSS_C_NO_NAME) { + gss_stat = ssh->gsslib->import_name(ssh->gsslib, + ssh->fullhostname, + &ssh->gss_srv_name); + if (gss_stat != SSH_GSS_OK) { + if (gss_stat == SSH_GSS_BAD_HOST_NAME) + logevent("GSSAPI import name failed -" + " Bad service name; won't use GSS key exchange"); + else + logevent("GSSAPI import name failed;" + " won't use GSS key exchange"); + return; + } + } + + /* + * Do we (still) have credentials? Capture the credential expiration when + * available + */ + gss_stat = ssh->gsslib->acquire_cred(ssh->gsslib, + &gss_ctx, + &gss_cred_expiry); + if (gss_stat != SSH_GSS_OK) + return; + + SSH_GSS_CLEAR_BUF(&gss_sndtok); + SSH_GSS_CLEAR_BUF(&gss_rcvtok); + + /* + * When acquire_cred yields no useful expiration, get a proxy for the cred + * expiration from the context expiration. + */ + gss_stat = ssh->gsslib->init_sec_context( + ssh->gsslib, &gss_ctx, ssh->gss_srv_name, + 0 /* don't delegate */, &gss_rcvtok, &gss_sndtok, + (gss_cred_expiry == GSS_NO_EXPIRATION ? &gss_cred_expiry : NULL), + &ssh->gss_ctxt_lifetime); + + /* This context was for testing only. */ + if (gss_ctx) + ssh->gsslib->release_cred(ssh->gsslib, &gss_ctx); + + if (gss_stat != SSH_GSS_OK && + gss_stat != SSH_GSS_S_CONTINUE_NEEDED) { + logeventf(ssh, "GSSAPI init sec context failed;" + " won't use GSS key exchange"); + return; + } + + if (gss_sndtok.length) + ssh->gsslib->free_tok(ssh->gsslib, &gss_sndtok); + + ssh->gss_status |= GSS_KEX_CAPABLE; + + /* + * When rekeying to cascade, avoding doing this too close to the context + * expiration time, since the key exchange might fail. + */ + if (ssh->gss_ctxt_lifetime < MIN_CTXT_LIFETIME) + ssh->gss_status |= GSS_CTXT_MAYFAIL; + + /* + * If we're not delegating credentials, rekeying is not used to refresh + * them. We must avoid setting GSS_CRED_UPDATED or GSS_CTXT_EXPIRES when + * credential delegation is disabled. + */ + if (conf_get_int(ssh->conf, CONF_gssapifwd) == 0) + return; + + if (ssh->gss_cred_expiry != GSS_NO_EXPIRATION && + difftime(gss_cred_expiry, ssh->gss_cred_expiry) > 0) + ssh->gss_status |= GSS_CRED_UPDATED; + + mins = conf_get_int(ssh->conf, CONF_gssapirekey); + mins = rekey_mins(mins, GSS_DEF_REKEY_MINS); + if (mins > 0 && ssh->gss_ctxt_lifetime <= mins * 60) + ssh->gss_status |= GSS_CTXT_EXPIRES; +} +#endif + /* * The rekey_time is zero except when re-configuring. * @@ -11165,6 +12017,30 @@ static int ssh2_timer_update(Ssh ssh, unsigned long rekey_time) ticks = next - now; } +#ifndef NO_GSSAPI + { + unsigned long gssmins; + + /* Check cascade conditions more frequently if configured */ + gssmins = conf_get_int(ssh->conf, CONF_gssapirekey); + gssmins = rekey_mins(gssmins, GSS_DEF_REKEY_MINS); + if (gssmins > 0) { + if (gssmins < mins) + ticks = (mins = gssmins) * 60 * TICKSPERSEC; + + if ((ssh->gss_status & GSS_KEX_CAPABLE) != 0) { + /* + * Run next timer even sooner if it would otherwise be too close + * to the context expiration time + */ + if ((ssh->gss_status & GSS_CTXT_EXPIRES) == 0 && + ssh->gss_ctxt_lifetime - mins * 60 < 2 * MIN_CTXT_LIFETIME) + ticks -= 2 * MIN_CTXT_LIFETIME * TICKSPERSEC; + } + } + } +#endif + /* Schedule the next timer */ ssh->next_rekey = schedule_timer(ticks, ssh2_timer, ssh); return 0; @@ -11173,15 +12049,45 @@ static int ssh2_timer_update(Ssh ssh, unsigned long rekey_time) static void ssh2_timer(void *ctx, unsigned long now) { Ssh ssh = (Ssh)ctx; + unsigned long mins; + unsigned long ticks; - if (ssh->state == SSH_STATE_CLOSED) + if (ssh->state == SSH_STATE_CLOSED || + ssh->kex_in_progress || + ssh->bare_connection || + now != ssh->next_rekey) return; - if (!ssh->kex_in_progress && !ssh->bare_connection && - conf_get_int(ssh->conf, CONF_ssh_rekey_time) != 0 && - now == ssh->next_rekey) { + mins = conf_get_int(ssh->conf, CONF_ssh_rekey_time); + mins = rekey_mins(mins, 60); + if (mins == 0) + return; + + /* Rekey if enough time has elapsed */ + ticks = mins * 60 * TICKSPERSEC; + if (now - ssh->last_rekey > ticks - 30*TICKSPERSEC) { do_ssh2_transport(ssh, "timeout", -1, NULL); + return; } + +#ifndef NO_GSSAPI + /* + * Rekey now if we have a new cred or context expires this cycle, but not if + * this is unsafe. + */ + if (conf_get_int(ssh->conf, CONF_gssapirekey)) { + ssh2_gss_update(ssh); + if ((ssh->gss_status & GSS_KEX_CAPABLE) != 0 && + (ssh->gss_status & GSS_CTXT_MAYFAIL) == 0 && + (ssh->gss_status & (GSS_CRED_UPDATED|GSS_CTXT_EXPIRES)) != 0) { + do_ssh2_transport(ssh, GSS_UPDATE_REKEY_REASON, -1, NULL); + return; + } + } +#endif + + /* Try again later. */ + (void) ssh2_timer_update(ssh, 0); } static void ssh2_protocol(Ssh ssh, const void *vin, int inlen, @@ -11315,6 +12221,14 @@ static const char *ssh_init(void *frontend_handle, void **backend_handle, ssh->n_uncert_hostkeys = 0; ssh->cross_certifying = FALSE; +#ifndef NO_GSSAPI + ssh->gss_cred_expiry = GSS_NO_EXPIRATION; + ssh->gss_srv_name = GSS_C_NO_NAME; + ssh->gss_ctx = NULL; + ssh_init_transient_hostkey_store(ssh); +#endif + ssh->gss_kex_used = FALSE; + *backend_handle = ssh; #ifdef MSCRYPTOAPI @@ -11478,8 +12392,13 @@ static void ssh_free(void *handle) agent_cancel_query(ssh->auth_agent_query); #ifndef NO_GSSAPI + if (ssh->gss_srv_name) + ssh->gsslib->release_name(ssh->gsslib, &ssh->gss_srv_name); + if (ssh->gss_ctx != NULL) + ssh->gsslib->release_cred(ssh->gsslib, &ssh->gss_ctx); if (ssh->gsslibs) ssh_gss_cleanup(ssh->gsslibs); + ssh_cleanup_transient_hostkey_store(ssh); #endif need_random_unref = ssh->need_random_unref; sfree(ssh); diff --git a/ssh.h b/ssh.h index 2d9c3550..a7902f2c 100644 --- a/ssh.h +++ b/ssh.h @@ -381,7 +381,7 @@ struct ssh_hash { struct ssh_kex { const char *name, *groupname; - enum { KEXTYPE_DH, KEXTYPE_RSA, KEXTYPE_ECDH } main_type; + enum { KEXTYPE_DH, KEXTYPE_RSA, KEXTYPE_ECDH, KEXTYPE_GSS } main_type; const struct ssh_hash *hash; const void *extra; /* private to the kex methods */ }; @@ -466,6 +466,7 @@ extern const struct ssh_hash ssh_sha512; extern const struct ssh_kexes ssh_diffiehellman_group1; extern const struct ssh_kexes ssh_diffiehellman_group14; extern const struct ssh_kexes ssh_diffiehellman_gex; +extern const struct ssh_kexes ssh_gssk5_sha1_kex; extern const struct ssh_kexes ssh_rsa_kex; extern const struct ssh_kexes ssh_ecdh_kex; extern const struct ssh_signkey ssh_dss; @@ -952,6 +953,13 @@ void platform_ssh_share_cleanup(const char *name); #define SSH2_MSG_KEX_DH_GEX_GROUP 31 /* 0x1f */ #define SSH2_MSG_KEX_DH_GEX_INIT 32 /* 0x20 */ #define SSH2_MSG_KEX_DH_GEX_REPLY 33 /* 0x21 */ +#define SSH2_MSG_KEXGSS_INIT 30 /* 0x1e */ +#define SSH2_MSG_KEXGSS_CONTINUE 31 /* 0x1f */ +#define SSH2_MSG_KEXGSS_COMPLETE 32 /* 0x20 */ +#define SSH2_MSG_KEXGSS_HOSTKEY 33 /* 0x21 */ +#define SSH2_MSG_KEXGSS_ERROR 34 /* 0x22 */ +#define SSH2_MSG_KEXGSS_GROUPREQ 40 /* 0x28 */ +#define SSH2_MSG_KEXGSS_GROUP 41 /* 0x29 */ #define SSH2_MSG_KEXRSA_PUBKEY 30 /* 0x1e */ #define SSH2_MSG_KEXRSA_SECRET 31 /* 0x1f */ #define SSH2_MSG_KEXRSA_DONE 32 /* 0x20 */ diff --git a/sshdh.c b/sshdh.c index 3942bd39..46f3a3fc 100644 --- a/sshdh.c +++ b/sshdh.c @@ -121,6 +121,46 @@ const struct ssh_kexes ssh_diffiehellman_gex = { gex_list }; +/* + * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5 + * as the mechanism. + * + * This suffix is the base64-encoded MD5 hash of the byte sequence + * 06 09 2A 86 48 86 F7 12 01 02 02, which in turn is the ASN.1 DER + * encoding of the object ID 1.2.840.113554.1.2.2 which designates + * Kerberos v5. + * + * (The same encoded OID, minus the two-byte DER header, is defined in + * pgssapi.c as GSS_MECH_KRB5.) + */ +#define GSS_KRB5_OID_HASH "toWM5Slw5Ew8Mqkay+al2g==" + +static const struct ssh_kex ssh_gssk5_diffiehellman_gex_sha1 = { + "gss-gex-sha1-" GSS_KRB5_OID_HASH, NULL, + KEXTYPE_GSS, &ssh_sha1, &extra_gex, +}; + +static const struct ssh_kex ssh_gssk5_diffiehellman_group14_sha1 = { + "gss-group14-sha1-" GSS_KRB5_OID_HASH, "group14", + KEXTYPE_GSS, &ssh_sha1, &extra_group14, +}; + +static const struct ssh_kex ssh_gssk5_diffiehellman_group1_sha1 = { + "gss-group1-sha1-" GSS_KRB5_OID_HASH, "group1", + KEXTYPE_GSS, &ssh_sha1, &extra_group1, +}; + +static const struct ssh_kex *const gssk5_sha1_kex_list[] = { + &ssh_gssk5_diffiehellman_gex_sha1, + &ssh_gssk5_diffiehellman_group14_sha1, + &ssh_gssk5_diffiehellman_group1_sha1 +}; + +const struct ssh_kexes ssh_gssk5_sha1_kex = { + sizeof(gssk5_sha1_kex_list) / sizeof(*gssk5_sha1_kex_list), + gssk5_sha1_kex_list +}; + /* * Variables. */ diff --git a/sshgss.h b/sshgss.h index 32ccb4db..11354948 100644 --- a/sshgss.h +++ b/sshgss.h @@ -13,6 +13,8 @@ typedef enum Ssh_gss_stat { SSH_GSS_S_CONTINUE_NEEDED, SSH_GSS_NO_MEM, SSH_GSS_BAD_HOST_NAME, + SSH_GSS_BAD_MIC, + SSH_GSS_NO_CREDS, SSH_GSS_FAILURE } Ssh_gss_stat; @@ -26,6 +28,8 @@ typedef enum Ssh_gss_stat { typedef gss_buffer_desc Ssh_gss_buf; typedef gss_name_t Ssh_gss_name; +#define GSS_NO_EXPIRATION ((time_t)-1) + /* Functions, provided by either wingss.c or sshgssc.c */ struct ssh_gss_library; @@ -79,7 +83,8 @@ typedef Ssh_gss_stat (*t_ssh_gss_release_name)(struct ssh_gss_library *lib, typedef Ssh_gss_stat (*t_ssh_gss_init_sec_context) (struct ssh_gss_library *lib, Ssh_gss_ctx *ctx, Ssh_gss_name name, int delegate, - Ssh_gss_buf *in, Ssh_gss_buf *out); + Ssh_gss_buf *in, Ssh_gss_buf *out, time_t *expiry, + unsigned long *lifetime); /* * Frees the contents of an Ssh_gss_buf filled in by @@ -96,7 +101,8 @@ typedef Ssh_gss_stat (*t_ssh_gss_free_tok)(struct ssh_gss_library *lib, * place. Needs to be freed by ssh_gss_release_cred(). */ typedef Ssh_gss_stat (*t_ssh_gss_acquire_cred)(struct ssh_gss_library *lib, - Ssh_gss_ctx *); + Ssh_gss_ctx *, + time_t *expiry); /* * Frees the contents of an Ssh_gss_ctx filled in by @@ -111,7 +117,15 @@ typedef Ssh_gss_stat (*t_ssh_gss_release_cred)(struct ssh_gss_library *lib, */ typedef Ssh_gss_stat (*t_ssh_gss_get_mic)(struct ssh_gss_library *lib, Ssh_gss_ctx ctx, Ssh_gss_buf *in, - Ssh_gss_buf *out); + Ssh_gss_buf *out); + +/* + * Validates an input MIC for some input data. + */ +typedef Ssh_gss_stat (*t_ssh_gss_verify_mic)(struct ssh_gss_library *lib, + Ssh_gss_ctx ctx, + Ssh_gss_buf *in_data, + Ssh_gss_buf *in_mic); /* * Frees the contents of an Ssh_gss_buf filled in by @@ -161,6 +175,7 @@ struct ssh_gss_library { t_ssh_gss_acquire_cred acquire_cred; t_ssh_gss_release_cred release_cred; t_ssh_gss_get_mic get_mic; + t_ssh_gss_verify_mic verify_mic; t_ssh_gss_free_mic free_mic; t_ssh_gss_display_status display_status; diff --git a/sshgssc.c b/sshgssc.c index 4590ed7b..26d301b7 100644 --- a/sshgssc.c +++ b/sshgssc.c @@ -1,6 +1,7 @@ #include "putty.h" #include +#include #include "sshgssc.h" #include "misc.h" @@ -38,14 +39,64 @@ static Ssh_gss_stat ssh_gssapi_import_name(struct ssh_gss_library *lib, } static Ssh_gss_stat ssh_gssapi_acquire_cred(struct ssh_gss_library *lib, - Ssh_gss_ctx *ctx) + Ssh_gss_ctx *ctx, + time_t *expiry) { + struct gssapi_functions *gss = &lib->u.gssapi; + gss_OID_set_desc k5only = { 1, GSS_MECH_KRB5 }; + gss_cred_id_t cred; + OM_uint32 dummy; + OM_uint32 time_rec; gssapi_ssh_gss_ctx *gssctx = snew(gssapi_ssh_gss_ctx); - gssctx->maj_stat = gssctx->min_stat = GSS_S_COMPLETE; gssctx->ctx = GSS_C_NO_CONTEXT; - *ctx = (Ssh_gss_ctx) gssctx; + gssctx->expiry = 0; + gssctx->maj_stat = + gss->acquire_cred(&gssctx->min_stat, GSS_C_NO_NAME, GSS_C_INDEFINITE, + &k5only, GSS_C_INITIATE, &cred, + (gss_OID_set *)0, &time_rec); + + if (gssctx->maj_stat != GSS_S_COMPLETE) { + sfree(gssctx); + return SSH_GSS_FAILURE; + } + + /* + * When the credential lifetime is not yet available due to deferred + * processing, gss_acquire_cred should return a 0 lifetime which is + * distinct from GSS_C_INDEFINITE which signals a crential that never + * expires. However, not all implementations get this right, and with + * Kerberos, initiator credentials always expire at some point. So when + * lifetime is 0 or GSS_C_INDEFINITE we call gss_inquire_cred_by_mech() to + * complete deferred processing. + */ + if (time_rec == GSS_C_INDEFINITE || time_rec == 0) { + gssctx->maj_stat = + gss->inquire_cred_by_mech(&gssctx->min_stat, cred, + (gss_OID) GSS_MECH_KRB5, + GSS_C_NO_NAME, + &time_rec, + NULL, + NULL); + } + (void) gss->release_cred(&dummy, &cred); + + if (gssctx->maj_stat != GSS_S_COMPLETE) { + sfree(gssctx); + return SSH_GSS_FAILURE; + } + + if (time_rec != GSS_C_INDEFINITE) + gssctx->expiry = time(NULL) + time_rec; + else + gssctx->expiry = GSS_NO_EXPIRATION; + + if (expiry) { + *expiry = gssctx->expiry; + } + + *ctx = (Ssh_gss_ctx) gssctx; return SSH_GSS_OK; } @@ -54,11 +105,14 @@ static Ssh_gss_stat ssh_gssapi_init_sec_context(struct ssh_gss_library *lib, Ssh_gss_name srv_name, int to_deleg, Ssh_gss_buf *recv_tok, - Ssh_gss_buf *send_tok) + Ssh_gss_buf *send_tok, + time_t *expiry, + unsigned long *lifetime) { struct gssapi_functions *gss = &lib->u.gssapi; gssapi_ssh_gss_ctx *gssctx = (gssapi_ssh_gss_ctx*) *ctx; OM_uint32 ret_flags; + OM_uint32 lifetime_rec; if (to_deleg) to_deleg = GSS_C_DELEG_FLAG; gssctx->maj_stat = gss->init_sec_context(&gssctx->min_stat, @@ -74,7 +128,20 @@ static Ssh_gss_stat ssh_gssapi_init_sec_context(struct ssh_gss_library *lib, NULL, /* ignore mech type */ send_tok, &ret_flags, - NULL); /* ignore time_rec */ + &lifetime_rec); + + if (lifetime) { + if (lifetime_rec == GSS_C_INDEFINITE) + *lifetime = ULONG_MAX; + else + *lifetime = lifetime_rec; + } + if (expiry) { + if (lifetime_rec == GSS_C_INDEFINITE) + *expiry = GSS_NO_EXPIRATION; + else + *expiry = time(NULL) + lifetime_rec; + } if (gssctx->maj_stat == GSS_S_COMPLETE) return SSH_GSS_S_COMPLETE; if (gssctx->maj_stat == GSS_S_CONTINUE_NEEDED) return SSH_GSS_S_CONTINUE_NEEDED; @@ -148,6 +215,7 @@ static Ssh_gss_stat ssh_gssapi_release_cred(struct ssh_gss_library *lib, if (gssctx->ctx != GSS_C_NO_CONTEXT) maj_stat = gss->delete_sec_context(&min_stat,&gssctx->ctx,GSS_C_NO_BUFFER); sfree(gssctx); + *ctx = NULL; if (maj_stat == GSS_S_COMPLETE) return SSH_GSS_OK; return SSH_GSS_FAILURE; @@ -175,6 +243,16 @@ static Ssh_gss_stat ssh_gssapi_get_mic(struct ssh_gss_library *lib, return gss->get_mic(&(gssctx->min_stat), gssctx->ctx, 0, buf, hash); } +static Ssh_gss_stat ssh_gssapi_verify_mic(struct ssh_gss_library *lib, + Ssh_gss_ctx ctx, Ssh_gss_buf *buf, + Ssh_gss_buf *hash) +{ + struct gssapi_functions *gss = &lib->u.gssapi; + gssapi_ssh_gss_ctx *gssctx = (gssapi_ssh_gss_ctx *) ctx; + if (gssctx == NULL) return SSH_GSS_FAILURE; + return gss->verify_mic(&(gssctx->min_stat), gssctx->ctx, buf, hash, NULL); +} + static Ssh_gss_stat ssh_gssapi_free_mic(struct ssh_gss_library *lib, Ssh_gss_buf *hash) { @@ -192,6 +270,7 @@ void ssh_gssapi_bind_fns(struct ssh_gss_library *lib) lib->acquire_cred = ssh_gssapi_acquire_cred; lib->release_cred = ssh_gssapi_release_cred; lib->get_mic = ssh_gssapi_get_mic; + lib->verify_mic = ssh_gssapi_verify_mic; lib->free_mic = ssh_gssapi_free_mic; lib->display_status = ssh_gssapi_display_status; } diff --git a/sshgssc.h b/sshgssc.h index c98ee86f..07fac009 100644 --- a/sshgssc.h +++ b/sshgssc.h @@ -10,6 +10,7 @@ typedef struct gssapi_ssh_gss_ctx { OM_uint32 maj_stat; OM_uint32 min_stat; gss_ctx_id_t ctx; + time_t expiry; } gssapi_ssh_gss_ctx; void ssh_gssapi_bind_fns(struct ssh_gss_library *lib); diff --git a/unix/uxgss.c b/unix/uxgss.c index 2a9e1296..47c59172 100644 --- a/unix/uxgss.c +++ b/unix/uxgss.c @@ -41,11 +41,14 @@ static void gss_init(struct ssh_gss_library *lib, void *dlhandle, BIND_GSS_FN(delete_sec_context); BIND_GSS_FN(display_status); BIND_GSS_FN(get_mic); + BIND_GSS_FN(verify_mic); BIND_GSS_FN(import_name); BIND_GSS_FN(init_sec_context); BIND_GSS_FN(release_buffer); BIND_GSS_FN(release_cred); BIND_GSS_FN(release_name); + BIND_GSS_FN(acquire_cred); + BIND_GSS_FN(inquire_cred_by_mech); #undef BIND_GSS_FN @@ -145,11 +148,14 @@ struct ssh_gss_liblist *ssh_gss_setup(Conf *conf) BIND_GSS_FN(delete_sec_context); BIND_GSS_FN(display_status); BIND_GSS_FN(get_mic); + BIND_GSS_FN(verify_mic); BIND_GSS_FN(import_name); BIND_GSS_FN(init_sec_context); BIND_GSS_FN(release_buffer); BIND_GSS_FN(release_cred); BIND_GSS_FN(release_name); + BIND_GSS_FN(acquire_cred); + BIND_GSS_FN(inquire_cred_by_mech); #undef BIND_GSS_FN diff --git a/windows/wingss.c b/windows/wingss.c index 2661ff76..c9bd2b31 100644 --- a/windows/wingss.c +++ b/windows/wingss.c @@ -1,5 +1,6 @@ #ifndef NO_GSSAPI +#include #include "putty.h" #define SECURITY_WIN32 @@ -11,6 +12,22 @@ #include "misc.h" +#define UNIX_EPOCH 11644473600ULL /* Seconds from Windows epoch */ +#define CNS_PERSEC 10000000ULL /* # 100ns per second */ + +/* + * Note, as a special case, 0 relative to the Windows epoch (unspecified) maps + * to 0 relative to the POSIX epoch (unspecified)! + */ +#define TIME_WIN_TO_POSIX(ft, t) do { \ + ULARGE_INTEGER uli; \ + uli.LowPart = (ft).dwLowDateTime; \ + uli.HighPart = (ft).dwHighDateTime; \ + if (uli.QuadPart != 0) \ + uli.QuadPart = uli.QuadPart / CNS_PERSEC - UNIX_EPOCH; \ + (t) = (time_t) uli.QuadPart; \ +} while(0) + /* Windows code to set up the GSSAPI library list. */ #ifdef _WIN64 @@ -55,6 +72,9 @@ DECL_WINDOWS_FUNCTION(static, SECURITY_STATUS, DECL_WINDOWS_FUNCTION(static, SECURITY_STATUS, MakeSignature, (PCtxtHandle, ULONG, PSecBufferDesc, ULONG)); +DECL_WINDOWS_FUNCTION(static, SECURITY_STATUS, + VerifySignature, + (PCtxtHandle, PSecBufferDesc, ULONG, PULONG)); DECL_WINDOWS_FUNCTION(static, DLL_DIRECTORY_COOKIE, AddDllDirectory, (PCWSTR)); @@ -144,6 +164,7 @@ struct ssh_gss_liblist *ssh_gss_setup(Conf *conf) BIND_GSS_FN(delete_sec_context); BIND_GSS_FN(display_status); BIND_GSS_FN(get_mic); + BIND_GSS_FN(verify_mic); BIND_GSS_FN(import_name); BIND_GSS_FN(init_sec_context); BIND_GSS_FN(release_buffer); @@ -172,6 +193,7 @@ struct ssh_gss_liblist *ssh_gss_setup(Conf *conf) GET_WINDOWS_FUNCTION(module, DeleteSecurityContext); GET_WINDOWS_FUNCTION(module, QueryContextAttributesA); GET_WINDOWS_FUNCTION(module, MakeSignature); + GET_WINDOWS_FUNCTION(module, VerifySignature); ssh_sspi_bind_fns(lib); } @@ -224,6 +246,7 @@ struct ssh_gss_liblist *ssh_gss_setup(Conf *conf) BIND_GSS_FN(delete_sec_context); BIND_GSS_FN(display_status); BIND_GSS_FN(get_mic); + BIND_GSS_FN(verify_mic); BIND_GSS_FN(import_name); BIND_GSS_FN(init_sec_context); BIND_GSS_FN(release_buffer); @@ -289,7 +312,8 @@ static Ssh_gss_stat ssh_sspi_import_name(struct ssh_gss_library *lib, } static Ssh_gss_stat ssh_sspi_acquire_cred(struct ssh_gss_library *lib, - Ssh_gss_ctx *ctx) + Ssh_gss_ctx *ctx, + time_t *expiry) { winSsh_gss_ctx *winctx = snew(winSsh_gss_ctx); memset(winctx, 0, sizeof(winSsh_gss_ctx)); @@ -309,21 +333,68 @@ static Ssh_gss_stat ssh_sspi_acquire_cred(struct ssh_gss_library *lib, NULL, NULL, &winctx->cred_handle, - &winctx->expiry); + NULL); + + if (winctx->maj_stat != SEC_E_OK) { + p_FreeCredentialsHandle(&winctx->cred_handle); + sfree(winctx); + return SSH_GSS_FAILURE; + } + + /* Windows does not return a valid expiration from AcquireCredentials */ + if (expiry) + *expiry = GSS_NO_EXPIRATION; - if (winctx->maj_stat != SEC_E_OK) return SSH_GSS_FAILURE; - *ctx = (Ssh_gss_ctx) winctx; return SSH_GSS_OK; } +static void localexp_to_exp_lifetime(TimeStamp *localexp, + time_t *expiry, unsigned long *lifetime) +{ + FILETIME nowUTC; + FILETIME expUTC; + time_t now; + time_t exp; + time_t delta; + + if (!lifetime && !expiry) + return; + + GetSystemTimeAsFileTime(&nowUTC); + TIME_WIN_TO_POSIX(nowUTC, now); + + if (lifetime) + *lifetime = 0; + if (expiry) + *expiry = GSS_NO_EXPIRATION; + + if (!LocalFileTimeToFileTime(localexp, &expUTC)) + return; + + TIME_WIN_TO_POSIX(expUTC, exp); + delta = exp - now; + if (exp == 0 || delta <= 0) + return; + + if (expiry) + *expiry = exp; + if (lifetime) { + if (delta <= ULONG_MAX) + *lifetime = (unsigned long)delta; + else + *lifetime = ULONG_MAX; + } +} static Ssh_gss_stat ssh_sspi_init_sec_context(struct ssh_gss_library *lib, Ssh_gss_ctx *ctx, Ssh_gss_name srv_name, int to_deleg, Ssh_gss_buf *recv_tok, - Ssh_gss_buf *send_tok) + Ssh_gss_buf *send_tok, + time_t *expiry, + unsigned long *lifetime) { winSsh_gss_ctx *winctx = (winSsh_gss_ctx *) *ctx; SecBuffer wsend_tok = {send_tok->length,SECBUFFER_TOKEN,send_tok->value}; @@ -333,6 +404,7 @@ static Ssh_gss_stat ssh_sspi_init_sec_context(struct ssh_gss_library *lib, unsigned long flags=ISC_REQ_MUTUAL_AUTH|ISC_REQ_REPLAY_DETECT| ISC_REQ_CONFIDENTIALITY|ISC_REQ_ALLOCATE_MEMORY; unsigned long ret_flags=0; + TimeStamp localexp; /* check if we have to delegate ... */ if (to_deleg) flags |= ISC_REQ_DELEGATE; @@ -347,8 +419,10 @@ static Ssh_gss_stat ssh_sspi_init_sec_context(struct ssh_gss_library *lib, &winctx->context, &output_desc, &ret_flags, - &winctx->expiry); - + &localexp); + + localexp_to_exp_lifetime(&localexp, expiry, lifetime); + /* prepare for the next round */ winctx->context_handle = &winctx->context; send_tok->value = wsend_tok.pvBuffer; @@ -503,6 +577,36 @@ static Ssh_gss_stat ssh_sspi_get_mic(struct ssh_gss_library *lib, return winctx->maj_stat; } +static Ssh_gss_stat ssh_sspi_verify_mic(struct ssh_gss_library *lib, + Ssh_gss_ctx ctx, + Ssh_gss_buf *buf, + Ssh_gss_buf *mic) +{ + winSsh_gss_ctx *winctx= (winSsh_gss_ctx *) ctx; + SecBufferDesc InputBufferDescriptor; + SecBuffer InputSecurityToken[2]; + ULONG qop; + + if (winctx == NULL) return SSH_GSS_FAILURE; + + winctx->maj_stat = 0; + + InputBufferDescriptor.cBuffers = 2; + InputBufferDescriptor.pBuffers = InputSecurityToken; + InputBufferDescriptor.ulVersion = SECBUFFER_VERSION; + InputSecurityToken[0].BufferType = SECBUFFER_DATA; + InputSecurityToken[0].cbBuffer = buf->length; + InputSecurityToken[0].pvBuffer = buf->value; + InputSecurityToken[1].BufferType = SECBUFFER_TOKEN; + InputSecurityToken[1].cbBuffer = mic->length; + InputSecurityToken[1].pvBuffer = mic->value; + + winctx->maj_stat = p_VerifySignature(&winctx->context, + &InputBufferDescriptor, + 0, &qop); + return winctx->maj_stat; +} + static Ssh_gss_stat ssh_sspi_free_mic(struct ssh_gss_library *lib, Ssh_gss_buf *hash) { @@ -520,6 +624,7 @@ static void ssh_sspi_bind_fns(struct ssh_gss_library *lib) lib->acquire_cred = ssh_sspi_acquire_cred; lib->release_cred = ssh_sspi_release_cred; lib->get_mic = ssh_sspi_get_mic; + lib->verify_mic = ssh_sspi_verify_mic; lib->free_mic = ssh_sspi_free_mic; lib->display_status = ssh_sspi_display_status; } diff --git a/windows/winhelp.h b/windows/winhelp.h index 3c5fb861..8e4e06fe 100644 --- a/windows/winhelp.h +++ b/windows/winhelp.h @@ -104,6 +104,7 @@ #define WINHELP_CTX_ssh_share "ssh.sharing:config-ssh-sharing" #define WINHELP_CTX_ssh_kexlist "ssh.kex.order:config-ssh-kex-order" #define WINHELP_CTX_ssh_hklist "ssh.hostkey.order:config-ssh-hostkey-order" +#define WINHELP_CTX_ssh_gssapi_kex_delegation "ssh.kex.gssapi.delegation:config-ssh-kex-gssapi-delegation" #define WINHELP_CTX_ssh_kex_repeat "ssh.kex.repeat:config-ssh-kex-rekey" #define WINHELP_CTX_ssh_kex_manual_hostkeys "ssh.kex.manualhostkeys:config-ssh-kex-manual-hostkeys" #define WINHELP_CTX_ssh_auth_bypass "ssh.auth.bypass:config-ssh-noauth"