diff --git a/openssh-9.9p1-gssapi-s4u.patch b/openssh-9.9p1-gssapi-s4u.patch new file mode 100644 index 0000000..7e94f6b --- /dev/null +++ b/openssh-9.9p1-gssapi-s4u.patch @@ -0,0 +1,1026 @@ +diff --git a/auth-krb5.c b/auth-krb5.c +index a37be93c3..34f058967 100644 +--- a/auth-krb5.c ++++ b/auth-krb5.c +@@ -465,6 +465,19 @@ ssh_krb5_cc_new_unique(krb5_context ctx, krb5_ccache *ccache, int *need_environm + * a primary cache for this collection, if it supports that (non-FILE) + */ + if (krb5_cc_support_switch(ctx, type)) { ++ /* ++ * For collection-type caches (KCM, KEYRING, …) reuse the ++ * existing primary ccache when one is already present. The ++ * caller will reinitialise it with krb5_cc_initialize(), so ++ * its old contents are replaced rather than orphaned. Only ++ * create a fresh unique ccache when no primary exists yet. ++ */ ++ if (krb5_cc_default(ctx, ccache) == 0) { ++ debug3_f("reusing existing default ccache of type %s", ++ type); ++ free(type); ++ return 0; ++ } + debug3_f("calling cc_new_unique(%s)", ccname); + ret = krb5_cc_new_unique(ctx, type, NULL, ccache); + free(type); +diff --git a/configure.ac b/configure.ac +index e33462027..22f806787 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -5102,11 +5102,17 @@ AC_ARG_WITH([kerberos5], + # include + #elif defined(HAVE_GSSAPI_GSSAPI_GENERIC_H) + # include ++#endif ++#ifdef HAVE_GSSAPI_EXT_H ++# include ++#endif ++#ifdef HAVE_GSSAPI_KRB5_H ++# include + #endif + ]]) + saved_LIBS="$LIBS" +- LIBS="$LIBS $K5LIBS" +- AC_CHECK_FUNCS([krb5_cc_new_unique krb5_get_error_message krb5_free_error_message]) ++ LIBS="$LIBS $GSSLIBS $K5LIBS " ++ AC_CHECK_FUNCS([krb5_cc_new_unique krb5_get_error_message krb5_free_error_message gss_acquire_cred_from]) + LIBS="$saved_LIBS" + + fi +diff --git a/gss-serv-krb5.c b/gss-serv-krb5.c +index 2c786ef14..64e327917 100644 +--- a/gss-serv-krb5.c ++++ b/gss-serv-krb5.c +@@ -590,6 +590,231 @@ ssh_gssapi_krb5_updatecreds(ssh_gssapi_ccache *store, + return 1; + } + ++/* ++ * Check whether the user's default ccache already contains valid service ++ * tickets for all principals listed in services[]. Returns 1 if every ++ * listed service has a ticket with at least min_lifetime seconds remaining ++ * (pass GSS_C_INDEFINITE to accept any positive remaining lifetime), 0 if ++ * any ticket is missing or too close to expiry. Runs as the user. ++ * ++ * Unlike ssh_gssapi_user_has_valid_tgt(), this cannot use gss_acquire_cred() ++ * because service tickets are not initiator credentials — GSSAPI only ++ * surfaces TGTs via that API. We iterate the ccache with the krb5 API ++ * directly instead. ++ */ ++int ++ssh_gssapi_user_has_valid_proxy_tickets(char **services, u_int nservices, ++ u_int min_lifetime) ++{ ++ krb5_context ctx = NULL; ++ krb5_ccache cc = NULL; ++ krb5_cc_cursor cursor; ++ krb5_creds cred; ++ krb5_principal svc_princ; ++ int *found = NULL; ++ u_int i; ++ int all_found = 0; ++ time_t now; ++ ++ if (nservices == 0) ++ return 0; ++ ++ if (krb5_init_context(&ctx) != 0) ++ return 0; ++ ++ found = xcalloc(nservices, sizeof(*found)); ++ now = time(NULL); ++ ++ if (krb5_cc_default(ctx, &cc) != 0) ++ goto out; ++ ++ if (krb5_cc_start_seq_get(ctx, cc, &cursor) != 0) { ++ krb5_cc_close(ctx, cc); ++ cc = NULL; ++ goto out; ++ } ++ ++ while (krb5_cc_next_cred(ctx, cc, &cursor, &cred) == 0) { ++ /* ++ * Use krb5_principal_compare() so that service names ++ * configured without an explicit realm (krb5_parse_name ++ * appends the default realm) still match the fully- ++ * qualified principal stored in the ccache. ++ */ ++ for (i = 0; i < nservices; i++) { ++ if (found[i]) ++ continue; ++ if (krb5_parse_name(ctx, services[i], ++ &svc_princ) != 0) ++ continue; ++ if (krb5_principal_compare(ctx, ++ cred.server, svc_princ)) { ++ krb5_deltat remaining = ++ cred.times.endtime - now; ++ if (remaining > 0 && ++ (min_lifetime == GSS_C_INDEFINITE || ++ (krb5_deltat)min_lifetime <= remaining)) ++ found[i] = 1; ++ } ++ krb5_free_principal(ctx, svc_princ); ++ } ++ krb5_free_cred_contents(ctx, &cred); ++ } ++ krb5_cc_end_seq_get(ctx, cc, &cursor); ++ krb5_cc_close(ctx, cc); ++ cc = NULL; ++ ++ all_found = 1; ++ for (i = 0; i < nservices; i++) { ++ if (!found[i]) { ++ all_found = 0; ++ break; ++ } ++ } ++out: ++ free(found); ++ if (ctx != NULL) ++ krb5_free_context(ctx); ++ return all_found; ++} ++ ++/* As user - called on fatal/exit */ ++void ++ssh_gssapi_cleanup_creds(void) ++{ ++ ssh_gssapi_ccache *store = ssh_gssapi_get_ccache(); ++ krb5_ccache ccache = NULL; ++ krb5_error_code problem; ++ ++ if (store->data != NULL) { ++ if ((problem = krb5_cc_resolve(store->data, ++ store->envval, &ccache))) { ++ debug_f("krb5_cc_resolve(): %.100s", ++ krb5_get_err_text(store->data, problem)); ++ } else if ((problem = krb5_cc_destroy(store->data, ccache))) { ++ debug_f("krb5_cc_destroy(): %.100s", ++ krb5_get_err_text(store->data, problem)); ++ } else { ++ krb5_free_context(store->data); ++ store->data = NULL; ++ } ++ } ++} ++ ++/* ++ * Filter the user's ccache by removing the ticket classes indicated by ++ * drop_flags (SSH_GSSAPI_CCFILTER_* bitmask). Each credential is ++ * categorised as one of: ++ * TGT - server principal matches "krbtgt/" prefix ++ * PROXY - server principal matches one of proxy_services[] ++ * SELF - everything else (the S4U2Self evidence ticket) ++ * Credentials in a flagged category are discarded; the rest are written ++ * back after reinitialising the ccache. Runs as user. ++ */ ++void ++ssh_gssapi_krb5_filter_ccache(u_int drop_flags, ++ char **proxy_services, u_int nproxy_services) ++{ ++ ssh_gssapi_ccache *store = ssh_gssapi_get_ccache(); ++ krb5_context ctx = (krb5_context)store->data; ++ krb5_ccache cc = NULL; ++ krb5_cc_cursor cursor; ++ krb5_creds *keep = NULL; ++ krb5_principal princ = NULL; ++ krb5_error_code problem; ++ char *srvname; ++ u_int i, nkeep = 0, cap = 0; ++ int is_tgt, is_proxy, drop; ++ ++ if (ctx == NULL || store->envval == NULL) ++ return; ++ ++ if ((problem = krb5_cc_resolve(ctx, store->envval, &cc)) != 0) { ++ debug_f("krb5_cc_resolve: %.100s", ++ krb5_get_err_text(ctx, problem)); ++ return; ++ } ++ if ((problem = krb5_cc_get_principal(ctx, cc, &princ)) != 0) { ++ debug_f("krb5_cc_get_principal: %.100s", ++ krb5_get_err_text(ctx, problem)); ++ krb5_cc_close(ctx, cc); ++ return; ++ } ++ if ((problem = krb5_cc_start_seq_get(ctx, cc, &cursor)) != 0) { ++ debug_f("krb5_cc_start_seq_get: %.100s", ++ krb5_get_err_text(ctx, problem)); ++ krb5_free_principal(ctx, princ); ++ krb5_cc_close(ctx, cc); ++ return; ++ } ++ ++ { ++ krb5_creds cred; ++ krb5_principal svc_princ; ++ while (krb5_cc_next_cred(ctx, cc, &cursor, &cred) == 0) { ++ is_tgt = is_proxy = 0; ++ if (krb5_unparse_name(ctx, cred.server, ++ &srvname) == 0) { ++ is_tgt = strncmp(srvname, "krbtgt/", 7) == 0; ++ krb5_free_unparsed_name(ctx, srvname); ++ } ++ if (!is_tgt) { ++ /* ++ * Use krb5_principal_compare() rather than ++ * strcmp() so that a service name configured ++ * without an explicit realm (krb5_parse_name ++ * appends the default realm) still matches ++ * the fully-qualified name in the ccache. ++ */ ++ for (i = 0; i < nproxy_services; i++) { ++ if (krb5_parse_name(ctx, ++ proxy_services[i], &svc_princ) != 0) ++ continue; ++ if (krb5_principal_compare(ctx, ++ cred.server, svc_princ)) ++ is_proxy = 1; ++ krb5_free_principal(ctx, svc_princ); ++ if (is_proxy) ++ break; ++ } ++ } ++ if (is_tgt) ++ drop = drop_flags & SSH_GSSAPI_CCFILTER_TGT; ++ else if (is_proxy) ++ drop = drop_flags & SSH_GSSAPI_CCFILTER_PROXY; ++ else ++ drop = drop_flags & SSH_GSSAPI_CCFILTER_SELF; ++ ++ if (!drop) { ++ if (nkeep >= cap) { ++ cap = cap ? cap * 2 : 4; ++ keep = xreallocarray(keep, cap, ++ sizeof(*keep)); ++ } ++ keep[nkeep++] = cred; ++ } else ++ krb5_free_cred_contents(ctx, &cred); ++ } ++ } ++ krb5_cc_end_seq_get(ctx, cc, &cursor); ++ ++ if ((problem = krb5_cc_initialize(ctx, cc, princ)) != 0) { ++ logit_f("krb5_cc_initialize: %.100s", ++ krb5_get_err_text(ctx, problem)); ++ } else { ++ for (i = 0; i < nkeep; i++) ++ krb5_cc_store_cred(ctx, cc, &keep[i]); ++ debug_f("ccache filter 0x%x: retained %u ticket(s)", ++ drop_flags, nkeep); ++ } ++ ++ for (i = 0; i < nkeep; i++) ++ krb5_free_cred_contents(ctx, &keep[i]); ++ free(keep); ++ krb5_free_principal(ctx, princ); ++ krb5_cc_close(ctx, cc); ++} ++ + ssh_gssapi_mech gssapi_kerberos_mech = { + "toWM5Slw5Ew8Mqkay+al2g==", + "Kerberos", +diff --git a/gss-serv.c b/gss-serv.c +index 165484db0..90e2d03ce 100644 +--- a/gss-serv.c ++++ b/gss-serv.c +@@ -54,7 +54,7 @@ extern ServerOptions options; + + static ssh_gssapi_client gssapi_client = + { GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, GSS_C_NO_CREDENTIAL, +- GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL}; ++ GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL, 0}; + + ssh_gssapi_mech gssapi_null_mech = + { NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL, NULL}; +@@ -484,26 +484,351 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_gssapi_client *client) + return (ctx->major); + } + +-/* As user - called on fatal/exit */ ++/* Returns non-zero if Kerberos credentials have already been stored. */ ++int ++ssh_gssapi_credentials_stored(void) ++{ ++ return gssapi_client.store.envval != NULL; ++} ++ ++/* Returns a pointer to the credential-cache descriptor for this session. */ ++ssh_gssapi_ccache * ++ssh_gssapi_get_ccache(void) ++{ ++ return &gssapi_client.store; ++} ++ ++/* Log human-readable GSSAPI major and minor status strings. */ ++static void ++log_gss_error(OM_uint32 major, OM_uint32 minor, const char *label) ++{ ++ OM_uint32 lmin, mctx; ++ gss_buffer_desc emsg = GSS_C_EMPTY_BUFFER; ++ ++ mctx = 0; ++ do { ++ gss_display_status(&lmin, major, GSS_C_GSS_CODE, ++ GSS_C_NO_OID, &mctx, &emsg); ++ logit("%s: %.*s", label, (int)emsg.length, (char *)emsg.value); ++ gss_release_buffer(&lmin, &emsg); ++ } while (mctx != 0); ++ ++ mctx = 0; ++ do { ++ gss_display_status(&lmin, minor, GSS_C_MECH_CODE, ++ &gssapi_kerberos_mech.oid, &mctx, &emsg); ++ if (emsg.length > 0) ++ logit("%s: %.*s", label, ++ (int)emsg.length, (char *)emsg.value); ++ gss_release_buffer(&lmin, &emsg); ++ } while (mctx != 0); ++} ++ ++/* Log the canonical string form of a GSSAPI name as a debug message. */ ++static void ++debug_gss_name(const char *label, gss_name_t name) ++{ ++ OM_uint32 lmin; ++ gss_buffer_desc buf = GSS_C_EMPTY_BUFFER; ++ ++ if (gss_display_name(&lmin, name, &buf, NULL) == GSS_S_COMPLETE) { ++ debug_f("%s: %.*s", label, (int)buf.length, (char *)buf.value); ++ gss_release_buffer(&lmin, &buf); ++ } ++} ++ ++/* ++ * Check whether the user already has valid GSSAPI initiator credentials ++ * (e.g. a Kerberos TGT) in their default credential store with at least ++ * min_lifetime seconds remaining. Pass GSS_C_INDEFINITE to accept any ++ * positive remaining lifetime. Runs as the user. ++ * Returns 1 if sufficient credentials exist, 0 otherwise. ++ */ ++int ++ssh_gssapi_user_has_valid_tgt(u_int min_lifetime) ++{ ++ OM_uint32 major, minor, lifetime = 0; ++ gss_cred_id_t cred = GSS_C_NO_CREDENTIAL; ++ int found = 0; ++ ++ major = gss_acquire_cred(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, ++ GSS_C_NO_OID_SET, GSS_C_INITIATE, &cred, NULL, &lifetime); ++ if (!GSS_ERROR(major) && lifetime > 0 && ++ (min_lifetime == GSS_C_INDEFINITE || lifetime >= min_lifetime)) ++ found = 1; ++ if (cred != GSS_C_NO_CREDENTIAL) ++ gss_release_cred(&minor, &cred); ++ return found; ++} ++ ++ ++/* ++ * Perform S4U2Self (protocol transition): acquire a Kerberos service ticket ++ * for the SSH user on behalf of the host principal. Runs privileged. ++ * Populates gssapi_client.{creds,mech,displayname,exportedname} on success. ++ * Returns 0 on success, -1 on failure. ++ */ ++/* Privileged */ ++int ++ssh_gssapi_s4u2self(const char *user, u_int lifetime) ++{ ++ OM_uint32 major, minor, status; ++ gss_OID_set oidset = GSS_C_NO_OID_SET; ++ gss_name_t host_name = GSS_C_NO_NAME; ++ gss_name_t user_name = GSS_C_NO_NAME; ++ gss_cred_id_t host_creds = GSS_C_NO_CREDENTIAL; ++ gss_cred_id_t impersonated_creds = GSS_C_NO_CREDENTIAL; ++ gss_buffer_desc gssbuf, displayname = GSS_C_EMPTY_BUFFER; ++ char lname[NI_MAXHOST]; ++ char *val; ++ ++ if (gethostname(lname, sizeof(lname)) != 0) { ++ logit_f("gethostname: %s", strerror(errno)); ++ return -1; ++ } ++ ++ /* Acquire acceptor credential for host/ from the keytab */ ++ gss_create_empty_oid_set(&status, &oidset); ++ gss_add_oid_set_member(&status, &gssapi_kerberos_mech.oid, &oidset); ++ ++ xasprintf(&val, "host@%s", lname); ++ gssbuf.value = val; ++ gssbuf.length = strlen(val); ++ major = gss_import_name(&minor, &gssbuf, ++ GSS_C_NT_HOSTBASED_SERVICE, &host_name); ++ free(val); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name (host) failed"); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ debug_gss_name("host name parsed as", host_name); ++ ++ debug_f("acquiring host credentials as uid=%u euid=%u, principal=host@%s", ++ (unsigned)getuid(), (unsigned)geteuid(), lname); ++#ifdef HAVE_GSS_ACQUIRE_CRED_FROM ++ { ++ gss_key_value_element_desc store_elements[] = { ++ { "client_keytab", "/etc/krb5.keytab" }, ++ { "keytab", "/etc/krb5.keytab" }, ++ { "ccache", "MEMORY:" }, ++ }; ++ const gss_key_value_set_desc cred_store = { 3, store_elements }; ++ ++ major = gss_acquire_cred_from(&minor, host_name, lifetime, ++ oidset, GSS_C_BOTH, &cred_store, &host_creds, NULL, NULL); ++ } ++#else ++ major = gss_acquire_cred(&minor, host_name, lifetime, ++ oidset, GSS_C_BOTH, &host_creds, NULL, NULL); ++#endif ++ gss_release_name(&minor, &host_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_acquire_cred(host@%s) failed as uid=%u euid=%u", ++ lname, (unsigned)getuid(), (unsigned)geteuid()); ++ log_gss_error(major, minor, "S4U2Self: gss_acquire_cred"); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ ++ /* Import the SSH username as a GSSAPI/Kerberos name */ ++ gssbuf.value = (void *)user; ++ gssbuf.length = strlen(user); ++ major = gss_import_name(&minor, &gssbuf, ++ GSS_C_NT_USER_NAME, &user_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name (user) failed"); ++ gss_release_cred(&minor, &host_creds); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ debug_gss_name("user name parsed as", user_name); ++ ++ /* S4U2Self: obtain a service ticket for the user without their creds */ ++ debug_f("calling gss_acquire_cred_impersonate_name for user %.100s", user); ++ major = gss_acquire_cred_impersonate_name(&minor, ++ host_creds, user_name, lifetime, ++ oidset, GSS_C_INITIATE, ++ &impersonated_creds, NULL, NULL); ++ ++ gss_release_cred(&minor, &host_creds); ++ gss_release_oid_set(&status, &oidset); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_acquire_cred_impersonate_name failed for %.100s", ++ user); ++ log_gss_error(major, minor, ++ "S4U2Self: gss_acquire_cred_impersonate_name"); ++ gss_release_name(&minor, &user_name); ++ return -1; ++ } ++ ++ /* Get the display name (Kerberos principal string) for storecreds */ ++ major = gss_display_name(&minor, user_name, &displayname, NULL); ++ gss_release_name(&minor, &user_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_display_name failed"); ++ gss_release_cred(&minor, &impersonated_creds); ++ return -1; ++ } ++ ++ /* Populate gssapi_client for storecreds_s4u2self and s4u2proxy */ ++ gssapi_client.mech = &gssapi_kerberos_mech; ++ gssapi_client.creds = impersonated_creds; ++ gssapi_client.displayname.value = xmalloc(displayname.length + 1); ++ memcpy(gssapi_client.displayname.value, ++ displayname.value, displayname.length); ++ ((char *)gssapi_client.displayname.value)[displayname.length] = '\0'; ++ gssapi_client.displayname.length = displayname.length; ++ /* ++ * exportedname is used by ssh_gssapi_krb5_storecreds → krb5_parse_name. ++ * gss_display_name for a user-name returns the canonical principal ++ * string (e.g. user@REALM) which krb5_parse_name can consume directly. ++ */ ++ gssapi_client.exportedname.value = xmalloc(displayname.length + 1); ++ memcpy(gssapi_client.exportedname.value, ++ displayname.value, displayname.length); ++ ((char *)gssapi_client.exportedname.value)[displayname.length] = '\0'; ++ gssapi_client.exportedname.length = displayname.length; ++ ++ gss_release_buffer(&minor, &displayname); ++ debug_f("S4U2Self succeeded for %.100s", user); ++ return 0; ++} ++ ++/* As user — write the S4U2Self ticket into a new ccache via mech->storecreds */ + void +-ssh_gssapi_cleanup_creds(void) ++ssh_gssapi_storecreds_s4u2self(void) + { +- krb5_ccache ccache = NULL; +- krb5_error_code problem; +- +- if (gssapi_client.store.data != NULL) { +- if ((problem = krb5_cc_resolve(gssapi_client.store.data, gssapi_client.store.envval, &ccache))) { +- debug_f("krb5_cc_resolve(): %.100s", +- krb5_get_err_text(gssapi_client.store.data, problem)); +- } else if ((problem = krb5_cc_destroy(gssapi_client.store.data, ccache))) { +- debug_f("krb5_cc_destroy(): %.100s", +- krb5_get_err_text(gssapi_client.store.data, problem)); +- } else { +- krb5_free_context(gssapi_client.store.data); +- gssapi_client.store.data = NULL; ++ if (gssapi_client.mech == NULL || gssapi_client.mech->storecreds == NULL) { ++ debug_f("no GSSAPI mechanism for storing S4U2Self credentials"); ++ return; ++ } ++ (*gssapi_client.mech->storecreds)(&gssapi_client); ++} ++ ++/* ++ * Perform S4U2Proxy for each configured service principal, then flush all ++ * resulting tickets into the user's ccache. Runs as user, after ++ * ssh_gssapi_storecreds_s4u2self() has created the ccache. ++ * ++ * gssapi_client.creds (the S4U2Self proxy credential) is passed as the ++ * initiator to gss_init_sec_context(); the GSSAPI library presents the TGT ++ * and evidence ticket to the KDC via S4U2Proxy TGS-REQ. The output token ++ * (AP-REQ) is discarded — we do not connect to the target service. ++ * ++ * After iterating all services, gss_store_cred() flushes the accumulated ++ * proxy service tickets from the credential's internal ccache into the ++ * KRB5CCNAME ccache that storecreds_s4u2self() already created. ++ */ ++/* As user */ ++void ++ssh_gssapi_s4u2proxy(char **services, u_int nservices, u_int lifetime) ++{ ++ OM_uint32 major, minor; ++ gss_buffer_desc service_buf, output_token = GSS_C_EMPTY_BUFFER; ++ gss_name_t target_name; ++ gss_ctx_id_t ctx; ++ u_int i; ++ ++ if (gssapi_client.creds == GSS_C_NO_CREDENTIAL) { ++ debug_f("no proxy credential available"); ++ return; ++ } ++ if (gssapi_client.store.envval == NULL) { ++ debug_f("no ccache path set; cannot store proxy tickets"); ++ return; ++ } ++ ++ debug_f("starting S4U2Proxy as uid=%u euid=%u, %u service(s), ccache=%s", ++ (unsigned)getuid(), (unsigned)geteuid(), nservices, ++ gssapi_client.store.envval); ++ ++ /* Point the GSSAPI library at the user's ccache for ticket storage */ ++ setenv("KRB5CCNAME", gssapi_client.store.envval, 1); ++ ++ for (i = 0; i < nservices; i++) { ++ ctx = GSS_C_NO_CONTEXT; ++ target_name = GSS_C_NO_NAME; ++ ++ service_buf.value = services[i]; ++ service_buf.length = strlen(services[i]); ++ ++ /* ++ * GSS_C_NO_OID: let the library determine the name type. ++ * With Kerberos as the active mechanism, a fully-qualified ++ * principal like "svc/host@REALM" is parsed correctly. ++ */ ++ major = gss_import_name(&minor, &service_buf, ++ GSS_C_NO_OID, &target_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name failed for %.200s", ++ services[i]); ++ log_gss_error(major, minor, "S4U2Proxy: gss_import_name"); ++ continue; + } ++ debug_gss_name("target service name parsed as", target_name); ++ ++ debug_f("calling gss_init_sec_context for %.200s", services[i]); ++ major = gss_init_sec_context(&minor, ++ gssapi_client.creds, /* proxy credential */ ++ &ctx, target_name, ++ GSS_C_NO_OID, /* default mech (Kerberos) */ ++ 0, /* no flags, no mutual auth */ ++ lifetime, ++ GSS_C_NO_CHANNEL_BINDINGS, ++ GSS_C_NO_BUFFER, /* no input token */ ++ NULL, /* actual_mech_type */ ++ &output_token, ++ NULL, /* ret_flags */ ++ NULL); /* time_rec */ ++ ++ gss_release_buffer(&minor, &output_token); ++ gss_release_name(&minor, &target_name); ++ if (ctx != GSS_C_NO_CONTEXT) ++ gss_delete_sec_context(&minor, &ctx, GSS_C_NO_BUFFER); ++ ++ if (GSS_ERROR(major)) { ++ logit_f("S4U2Proxy for %.200s on behalf of %.200s failed", ++ services[i], ++ (char *)gssapi_client.displayname.value); ++ log_gss_error(major, minor, ++ "S4U2Proxy: gss_init_sec_context"); ++ } else ++ debug_f("S4U2Proxy ticket obtained for %.200s", ++ services[i]); + } ++ ++ /* ++ * Flush all proxy service tickets from the credential's internal ++ * ccache into the KRB5CCNAME ccache via gss_store_cred(). ++ */ ++ major = gss_store_cred(&minor, gssapi_client.creds, GSS_C_INITIATE, ++ GSS_C_NO_OID, 1 /* overwrite_cred */, 1 /* default_cred */, ++ NULL, NULL); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_store_cred failed; proxy tickets may be missing"); ++ log_gss_error(major, minor, "S4U2Proxy: gss_store_cred"); ++ } ++ ++ unsetenv("KRB5CCNAME"); ++} ++ ++#ifndef KRB5 ++/* As user - called on fatal/exit; full implementation in gss-serv-krb5.c */ ++void ++ssh_gssapi_cleanup_creds(void) ++{ ++} ++ ++/* ++ * Filter the user's ccache; full implementation in gss-serv-krb5.c. ++ */ ++void ++ssh_gssapi_krb5_filter_ccache(u_int drop_flags, ++ char **proxy_services, u_int nproxy_services) ++{ + } ++#endif /* !KRB5 */ + + /* As user */ + int +diff --git a/servconf.c b/servconf.c +index 1093dcbac..019070505 100644 +--- a/servconf.c ++++ b/servconf.c +@@ -149,6 +149,9 @@ initialize_server_options(ServerOptions *options) + options->gss_store_rekey = -1; + options->gss_kex_algorithms = NULL; + options->gss_indicators = NULL; ++ options->gss_allow_s4u2self = -1; ++ options->gss_proxy_services = NULL; ++ options->num_gss_proxy_services = 0; + options->use_kuserok = -1; + options->enable_k5users = -1; + options->password_authentication = -1; +@@ -404,6 +407,8 @@ fill_default_server_options(ServerOptions *options) + options->gss_deleg_creds = 1; + if (options->gss_strict_acceptor == -1) + options->gss_strict_acceptor = 1; ++ if (options->gss_allow_s4u2self == -1) ++ options->gss_allow_s4u2self = 0; + if (options->gss_store_rekey == -1) + options->gss_store_rekey = 0; + #ifdef GSSAPI +@@ -599,7 +604,7 @@ typedef enum { + sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, + sGssAuthentication, sGssCleanupCreds, sGssDelegateCreds, + sGssEnablek5users, sGssStrictAcceptor, +- sGssKeyEx, sGssIndicators, sGssKexAlgorithms, sGssStoreRekey, ++ sGssKeyEx, sGssIndicators, sGssAllowS4U2Self, sGssProxyS4U2Services, sGssKexAlgorithms, sGssStoreRekey, + sAcceptEnv, sSetEnv, sPermitTunnel, + sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, + sUsePrivilegeSeparation, sAllowAgentForwarding, +@@ -697,6 +702,8 @@ static struct { + { "gssapikexalgorithms", sGssKexAlgorithms, SSHCFG_GLOBAL }, + { "gssapienablek5users", sGssEnablek5users, SSHCFG_ALL }, + { "gssapiindicators", sGssIndicators, SSHCFG_ALL }, ++ { "gssapiallows4u2self", sGssAllowS4U2Self, SSHCFG_ALL }, ++ { "gssapiproxys4u2services", sGssProxyS4U2Services, SSHCFG_ALL }, + #else + { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, + { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, +@@ -708,6 +715,8 @@ static struct { + { "gssapikexalgorithms", sUnsupported, SSHCFG_GLOBAL }, + { "gssapienablek5users", sUnsupported, SSHCFG_ALL }, + { "gssapiindicators", sUnsupported, SSHCFG_ALL }, ++ { "gssapiallows4u2self", sUnsupported, SSHCFG_ALL }, ++ { "gssapiproxys4u2services", sUnsupported, SSHCFG_ALL }, + #endif + { "gssusesessionccache", sUnsupported, SSHCFG_GLOBAL }, + { "gssapiusesessioncredcache", sUnsupported, SSHCFG_GLOBAL }, +@@ -1748,6 +1757,44 @@ process_server_config_line_depth(ServerOptions *options, char *line, + options->gss_indicators = xstrdup(arg); + break; + ++ case sGssAllowS4U2Self: ++ arg = argv_next(&ac, &av); ++ if (!arg || *arg == '\0') ++ fatal("%s line %d: %s missing argument.", ++ filename, linenum, keyword); ++ if (strcasecmp(arg, "no") == 0) ++ value = 0; ++ else if (strcasecmp(arg, "yes") == 0) ++ value = INT_MAX; ++ else if ((value = convtime(arg)) <= 0) ++ fatal("%s line %d: invalid %s value \"%s\".", ++ filename, linenum, keyword, arg); ++ if (*activep && options->gss_allow_s4u2self == -1) ++ options->gss_allow_s4u2self = value; ++ break; ++ ++ case sGssProxyS4U2Services: ++ while ((arg = argv_next(&ac, &av)) != NULL) { ++ if (*arg == '\0') ++ fatal("%s line %d: %s missing argument.", ++ filename, linenum, keyword); ++ if (strcasecmp(arg, "none") == 0) { ++ /* "none" clears any previous list */ ++ for (i = 0; i < options->num_gss_proxy_services; i++) ++ free(options->gss_proxy_services[i]); ++ free(options->gss_proxy_services); ++ options->gss_proxy_services = NULL; ++ options->num_gss_proxy_services = 0; ++ break; ++ } ++ if (!*activep) ++ continue; ++ opt_array_append(filename, linenum, keyword, ++ &options->gss_proxy_services, ++ &options->num_gss_proxy_services, arg); ++ } ++ break; ++ + case sPasswordAuthentication: + intptr = &options->password_authentication; + goto parse_flag; +@@ -3026,6 +3073,7 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth) + + M_CP_INTOPT(password_authentication); + M_CP_INTOPT(gss_authentication); ++ M_CP_INTOPT(gss_allow_s4u2self); + M_CP_INTOPT(pubkey_authentication); + M_CP_INTOPT(pubkey_auth_options); + M_CP_INTOPT(kerberos_authentication); +@@ -3379,6 +3427,15 @@ dump_config(ServerOptions *o) + dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey); + dump_cfg_string(sGssKexAlgorithms, o->gss_kex_algorithms); + dump_cfg_string(sGssIndicators, o->gss_indicators); ++ if (o->gss_allow_s4u2self == 0) ++ printf("%s no\n", lookup_opcode_name(sGssAllowS4U2Self)); ++ else if (o->gss_allow_s4u2self == INT_MAX) ++ printf("%s yes\n", lookup_opcode_name(sGssAllowS4U2Self)); ++ else ++ printf("%s %d\n", lookup_opcode_name(sGssAllowS4U2Self), ++ o->gss_allow_s4u2self); ++ dump_cfg_strarray_oneline(sGssProxyS4U2Services, o->num_gss_proxy_services, ++ o->gss_proxy_services); + #endif + dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication); + dump_cfg_fmtint(sKbdInteractiveAuthentication, +diff --git a/servconf.h b/servconf.h +index 6bfdf6305..40dcd29d8 100644 +--- a/servconf.h ++++ b/servconf.h +@@ -160,6 +160,9 @@ typedef struct { + int gss_cleanup_creds; /* If true, destroy cred cache on logout */ + int gss_deleg_creds; /* If true, accept delegated GSS credentials */ + int gss_strict_acceptor; /* If true, restrict the GSSAPI acceptor name */ ++ int gss_allow_s4u2self; /* 0=no, INT_MAX=yes (GSS_C_INDEFINITE), >0=ticket lifetime s */ ++ char **gss_proxy_services; /* S4U2Proxy target service principals */ ++ u_int num_gss_proxy_services; + int gss_store_rekey; + char *gss_kex_algorithms; /* GSSAPI kex methods to be offered by client. */ + int password_authentication; /* If true, permit password +@@ -313,6 +316,7 @@ TAILQ_HEAD(include_list, include_item); + M_CP_STROPT(permit_user_env_allowlist); \ + M_CP_STROPT(pam_service_name); \ + M_CP_STROPT(gss_indicators); \ ++ M_CP_STRARRAYOPT(gss_proxy_services, num_gss_proxy_services); \ + M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \ + M_CP_STRARRAYOPT(allow_users, num_allow_users); \ + M_CP_STRARRAYOPT(deny_users, num_deny_users); \ +diff --git a/ssh-gss.h b/ssh-gss.h +index 1506719a9..a0d51c9be 100644 +--- a/ssh-gss.h ++++ b/ssh-gss.h +@@ -119,6 +119,7 @@ typedef struct { + int used; + int updated; + char **indicators; /* auth indicators */ ++ int allow_self; /* allow protocol transition */ + } ssh_gssapi_client; + + typedef struct ssh_gssapi_mech_struct { +@@ -199,6 +200,18 @@ OM_uint32 ssh_gssapi_checkmic(Gssctxt *, gss_buffer_t, gss_buffer_t); + void ssh_gssapi_do_child(char ***, u_int *); + void ssh_gssapi_cleanup_creds(void); + int ssh_gssapi_storecreds(void); ++int ssh_gssapi_credentials_stored(void); ++ssh_gssapi_ccache *ssh_gssapi_get_ccache(void); ++int ssh_gssapi_user_has_valid_tgt(u_int); ++int ssh_gssapi_user_has_valid_proxy_tickets(char **, u_int, u_int); ++int ssh_gssapi_s4u2self(const char *, u_int); ++void ssh_gssapi_storecreds_s4u2self(void); ++void ssh_gssapi_s4u2proxy(char **, u_int, u_int); ++/* Flags for ssh_gssapi_krb5_filter_ccache(): which ticket classes to remove */ ++#define SSH_GSSAPI_CCFILTER_TGT (1u << 0) /* krbtgt/... entries */ ++#define SSH_GSSAPI_CCFILTER_SELF (1u << 1) /* S4U2Self evidence ticket */ ++#define SSH_GSSAPI_CCFILTER_PROXY (1u << 2) /* S4U2Proxy service tickets */ ++void ssh_gssapi_krb5_filter_ccache(u_int, char **, u_int); + const char *ssh_gssapi_displayname(void); + + char *ssh_gssapi_server_mechanisms(void); +diff --git a/sshd-session.c b/sshd-session.c +index a558bbc33..d8b6af13f 100644 +--- a/sshd-session.c ++++ b/sshd-session.c +@@ -1425,6 +1425,104 @@ main(int ac, char **av) + authctxt->krb5_set_env = ssh_gssapi_storecreds(); + restore_uid(); + } ++ /* ++ * GSSAPIAllowS4U2Self / GSSAPIS42UProxyServices: if no credentials were stored ++ * above (i.e. no GSSAPI auth with delegation occurred), use S4U2Self ++ * to obtain an impersonated credential for the user, then optionally ++ * follow with S4U2Proxy for configured target services. ++ * ++ * GSSAPIAllowS4U2Self alone: store S4U2Self evidence ticket only; ++ * the host TGT is removed. ++ * GSSAPIS42UProxyServices alone: store host TGT and S4U2Proxy service ++ * tickets; the S4U2Self evidence ticket ++ * is removed. ++ * Both: store host TGT, S4U2Self evidence ticket, ++ * and all S4U2Proxy service tickets. ++ * ++ * When S4U2Proxy tickets are present the host TGT must remain in the ++ * ccache; applications check for TGT presence to determine whether ++ * Kerberos credentials are available. Only in GSSAPIAllowS4U2Self-alone ++ * mode (no proxy tickets) is the host TGT removed. ++ * ++ * Skip S4U2Self when the user already has credentials covering the ++ * requested lifetime: check for a valid TGT in the GSSAPIAllowS4U2Self- ++ * alone case, or for valid proxy tickets for every configured service ++ * otherwise. ++ */ ++ if ((options.gss_allow_s4u2self || options.num_gss_proxy_services > 0) && ++ !ssh_gssapi_credentials_stored()) { ++ u_int lifetime = (!options.gss_allow_s4u2self || ++ options.gss_allow_s4u2self == INT_MAX) ? ++ GSS_C_INDEFINITE : (u_int)options.gss_allow_s4u2self; ++ int skip = 0; ++ ++ temporarily_use_uid(authctxt->pw); ++ if (options.gss_allow_s4u2self && ++ options.num_gss_proxy_services == 0) { ++ /* S4U2Self-alone: skip if user already has a valid TGT */ ++ skip = ssh_gssapi_user_has_valid_tgt(lifetime); ++ } else if (options.num_gss_proxy_services > 0) { ++ /* ++ * Proxy-only or both: skip if every configured service ++ * already has a valid ticket in the user's ccache. ++ * Service tickets are not GSSAPI initiator credentials, ++ * so gss_acquire_cred() cannot be used; iterate the ++ * ccache with the krb5 API instead. ++ */ ++ skip = ssh_gssapi_user_has_valid_proxy_tickets( ++ options.gss_proxy_services, ++ options.num_gss_proxy_services, ++ lifetime); ++ } ++ restore_uid(); ++ ++ if (skip) { ++ debug_f("user %.100s already has valid Kerberos " ++ "credentials, skipping S4U2Self", ++ authctxt->user); ++ } else if (ssh_gssapi_s4u2self(authctxt->user, lifetime) == 0) { ++ u_int filter; ++ ++ temporarily_use_uid(authctxt->pw); ++ /* ++ * Always create the ccache via storecreds_s4u2self so ++ * that s4u2proxy has a ccache to store tickets into. ++ * gss_krb5_copy_ccache() copies the host service's own ++ * TGT along with the evidence ticket; filter_ccache ++ * removes the ticket classes that should not be kept. ++ */ ++ ssh_gssapi_storecreds_s4u2self(); ++ if (options.num_gss_proxy_services > 0) ++ ssh_gssapi_s4u2proxy( ++ options.gss_proxy_services, ++ options.num_gss_proxy_services, ++ lifetime); ++ ++ /* ++ * Remove the host TGT only in GSSAPIAllowS4U2Self-alone ++ * mode; when proxy tickets are present the TGT must ++ * stay so that applications recognise the ccache as ++ * holding live Kerberos credentials. ++ * Remove the S4U2Self evidence ticket in proxy-only ++ * mode (GSSAPIS42UProxyServices without GSSAPIAllowS4U2Self). ++ */ ++ filter = 0; ++ if (options.gss_allow_s4u2self && ++ options.num_gss_proxy_services == 0) ++ filter = SSH_GSSAPI_CCFILTER_TGT | ++ SSH_GSSAPI_CCFILTER_PROXY; ++ else if (!options.gss_allow_s4u2self) ++ filter = SSH_GSSAPI_CCFILTER_SELF; ++ if (filter != 0) ++ ssh_gssapi_krb5_filter_ccache(filter, ++ options.gss_proxy_services, ++ options.num_gss_proxy_services); ++ restore_uid(); ++ } else { ++ logit("S4U2Self failed for user %.100s, continuing", ++ authctxt->user); ++ } ++ } + #endif + #ifdef WITH_SELINUX + sshd_selinux_setup_exec_context(authctxt->pw->pw_name, +diff --git a/sshd_config.5 b/sshd_config.5 +index 3dbce55fc..d25b406fb 100644 +--- a/sshd_config.5 ++++ b/sshd_config.5 +@@ -832,6 +832,76 @@ FIDO2-based pre-authentication in FreeIPA, using FIDO2 USB and NFC tokens + .El + .Pp + The default is to not use GSSAPI authentication indicators for access decisions. ++.It Cm GSSAPIAllowS4U2Self ++Controls whether the SSH server performs a Kerberos protocol transition ++(S4U2Self) after a successful authentication using any other method. ++Accepted values are ++.Cm no , ++.Cm yes , ++or a time interval (see ++.Sx TIME FORMATS ++below). ++.Cm no ++disables S4U2Self entirely. ++.Cm yes ++enables S4U2Self and requests a ticket with the maximum lifetime ++permitted by the KDC. ++A time interval (e.g.\& ++.Cm 8h , ++.Cm 1d ) ++enables S4U2Self and requests a ticket valid for at most that duration. ++The option is a no-op when delegated GSSAPI credentials are already available. ++The obtained service ticket is stored in the default credentials cache and is ++accessible to any application that has access to the Kerberos host principal ++.Pq host/machine.fqdn@REALM ++credentials on the same host. ++.Pp ++The default is ++.Cm no . ++.It Cm GSSAPIS42UProxyServices ++Specifies a list of Kerberos service principals for which constrained ++delegation (S4U2Proxy) tickets should be obtained after a successful ++S4U2Self protocol transition. ++Each entry must be a fully-qualified Kerberos principal name of the form ++.Ar service/host@REALM . ++Multiple principals may be listed, separated by whitespace. ++The keyword ++.Cm none ++clears any previously set list. ++.Pp ++This option may be used independently of ++.Cm GSSAPIAllowS4U2Self . ++When S4U2Self succeeds, the server iterates the list and calls ++.Xr gss_init_sec_context 3 ++for each principal with the proxy credential obtained by S4U2Self as ++the initiator. ++The GSSAPI library presents the evidence ticket to the KDC via an ++S4U2Proxy TGS-REQ; if the host service holds the necessary constrained- ++delegation permission in the KDC, a service ticket from the user to ++the target service is issued. ++These tickets are accumulated and then flushed into the user's ccache ++via ++.Xr gss_store_cred 3 , ++so that any application running in the user's session can use them ++without further interaction. ++The AP-REQ output token of each ++.Xr gss_init_sec_context 3 ++call is discarded; no network connection to the target service is made. ++.Pp ++When used together with ++.Cm GSSAPIAllowS4U2Self , ++the TGT and S4U2Self ticket are also stored in the user's ccache in ++addition to the S4U2Proxy service tickets. ++When used alone (without ++.Cm GSSAPIAllowS4U2Self ) , ++only the S4U2Proxy service tickets are stored; the intermediate S4U2Self ++credential is not placed in the user's ccache. ++.Pp ++This option supports ++.Cm Match ++blocks, allowing per-user or per-host lists of delegation targets. ++.Pp ++The default is empty (no S4U2Proxy delegation is performed). + .It Cm HostbasedAcceptedAlgorithms + The default is handled system-wide by + .Xr crypto-policies 7 . diff --git a/openssh.spec b/openssh.spec index 57ed49f..480eaa9 100644 --- a/openssh.spec +++ b/openssh.spec @@ -43,7 +43,7 @@ Summary: An open source implementation of SSH protocol version 2 Name: openssh Version: %{openssh_ver} -Release: 20%{?dist} +Release: 21%{?dist} URL: http://www.openssh.com/portable.html Source0: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz Source1: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz.asc @@ -227,6 +227,7 @@ Patch1032: openssh-9.9p1-reject-cntrl-chars-in-username.patch Patch1033: openssh-9.9p1-reject-null-char-in-url-string.patch Patch1034: openssh-9.9p1-sshd-no-delegate-credentials.patch Patch1035: openssh-10.0-mlkem-nist-fips.patch +Patch1036: openssh-9.9p1-gssapi-s4u.patch License: BSD-3-Clause AND BSD-2-Clause AND ISC AND SSH-OpenSSH AND ssh-keyscan AND sprintf AND LicenseRef-Fedora-Public-Domain AND X11-distribute-modifications-variant Requires: /sbin/nologin @@ -427,6 +428,7 @@ gpgv2 --quiet --keyring %{SOURCE3} %{SOURCE1} %{SOURCE0} %patch -P 1033 -p1 -b .reject-null-char-in-url-string %patch -P 1034 -p1 -b .sshd-nogsscreds %patch -P 1035 -p1 -b .mlkem-nist-fips +%patch -P 1036 -p1 -b .gssapi-s4u %patch -P 100 -p1 -b .coverity @@ -707,6 +709,10 @@ test -f %{sysconfig_anaconda} && \ %attr(0755,root,root) %{_libdir}/sshtest/sk-dummy.so %changelog +* Wed Mar 11 2026 Dmitry Belyavskiy - 9.9p1-21 +- Implement obtaining Kerberos tickets on behalf of user on SSH authentication + Resolves: RHEL-92932 + * Wed Feb 25 2026 Dmitry Belyavskiy - 9.9p1-20 - Provide a way to skip unsupported ML-KEM hybrid algorithms in FIPS mode Resolves: RHEL-151579