/* * Copyright (C) 1996-2024 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ /* * DEBUG: 11 Hypertext Transfer Protocol (HTTP) */ #include "squid.h" #if HAVE_AUTH_MODULE_NEGOTIATE && HAVE_KRB5 && HAVE_GSSAPI #if USE_APPLE_KRB5 #define GSSKRB_APPLE_DEPRECATED(x) #endif #include "base64.h" #include "compat/krb5.h" #include "debug/Stream.h" #include "peer_proxy_negotiate_auth.h" #if HAVE_PROFILE_H #include #endif /* HAVE_PROFILE_H */ #if HAVE_ET_COM_ERR_H && !HAVE_KRB5_H #include #endif /* HAVE_COM_ERR_H */ #if HAVE_COM_ERR_H #include #endif /* HAVE_COM_ERR_H */ #if HAVE_GSSAPI_GSSAPI_H #include #elif HAVE_GSSAPI_H #include #endif /* HAVE_GSSAPI_H */ #if !USE_HEIMDAL_KRB5 #if HAVE_GSSAPI_GSSAPI_EXT_H #include #endif /* HAVE_GSSAPI_GSSAPI_EXT_H */ #if HAVE_GSSAPI_GSSAPI_KRB5_H #include #endif /* HAVE_GSSAPI_GSSAPI_KRB5_H */ #if HAVE_GSSAPI_GSSAPI_GENERIC_H #include #endif /* HAVE_GSSAPI_GSSAPI_GENERIC_H */ #endif /* !USE_HEIMDAL_KRB5 */ #ifndef gss_nt_service_name #define gss_nt_service_name GSS_C_NT_HOSTBASED_SERVICE #endif #if !HAVE_ERROR_MESSAGE && HAVE_KRB5_GET_ERROR_MESSAGE #define error_message(code) krb5_get_error_message(kparam.context,code) #elif !HAVE_ERROR_MESSAGE && HAVE_KRB5_GET_ERR_TEXT #define error_message(code) krb5_get_err_text(kparam.context,code) #elif !HAVE_ERROR_MESSAGE static char err_code[17]; const char *KRB5_CALLCONV error_message(long code) { snprintf(err_code,16,"%ld",code); return err_code; } #endif #ifndef gss_mech_spnego static gss_OID_desc _gss_mech_spnego = { 6, (void *) "\x2b\x06\x01\x05\x05\x02" }; gss_OID gss_mech_spnego = &_gss_mech_spnego; #endif #if USE_IBM_KERBEROS #include const char *KRB5_CALLCONV error_message(long code) { char *msg = nullptr; krb5_svc_get_msg(code, &msg); return msg; } #endif /* * Kerberos context and cache structure * Caches authentication details to reduce * number of authentication requests to kdc */ static struct kstruct { krb5_context context; krb5_ccache cc; } kparam = { nullptr, nullptr }; /* * krb5_create_cache creates a Kerberos file credential cache or a memory * credential cache if supported. The initial key for the principal * principal_name is extracted from the keytab keytab_filename. * * If keytab_filename is NULL the default will be used. * If principal_name is NULL the first working entry of the keytab will be used. */ int krb5_create_cache(char *keytab_filename, char *principal_name); /* * krb5_cleanup clears used Keberos memory */ void krb5_cleanup(void); /* * check_gss_err checks for gssapi error codes, extracts the error message * and prints it. */ int check_gss_err(OM_uint32 major_status, OM_uint32 minor_status, const char *function); int check_gss_err(OM_uint32 major_status, OM_uint32 minor_status, const char *function) { if (GSS_ERROR(major_status)) { OM_uint32 maj_stat, min_stat; OM_uint32 msg_ctx = 0; gss_buffer_desc status_string; char buf[1024]; size_t len; len = 0; msg_ctx = 0; while (!msg_ctx) { /* convert major status code (GSS-API error) to text */ maj_stat = gss_display_status(&min_stat, major_status, GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_ctx, &status_string); if (maj_stat == GSS_S_COMPLETE) { if (sizeof(buf) > len + status_string.length + 1) { memcpy(buf + len, status_string.value, status_string.length); len += status_string.length; } gss_release_buffer(&min_stat, &status_string); break; } gss_release_buffer(&min_stat, &status_string); } if (sizeof(buf) > len + 2) { strcpy(buf + len, ". "); len += 2; } msg_ctx = 0; while (!msg_ctx) { /* convert minor status code (underlying routine error) to text */ maj_stat = gss_display_status(&min_stat, minor_status, GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_ctx, &status_string); if (maj_stat == GSS_S_COMPLETE) { if (sizeof(buf) > len + status_string.length) { memcpy(buf + len, status_string.value, status_string.length); len += status_string.length; } gss_release_buffer(&min_stat, &status_string); break; } gss_release_buffer(&min_stat, &status_string); } debugs(11, 5, function << "failed: " << buf); return (1); } return (0); } void krb5_cleanup() { debugs(11, 5, "Cleanup kerberos context"); if (kparam.context) { if (kparam.cc) krb5_cc_destroy(kparam.context, kparam.cc); kparam.cc = nullptr; krb5_free_context(kparam.context); kparam.context = nullptr; } } int krb5_create_cache(char *kf, char *pn) { #define KT_PATH_MAX 256 #define MAX_RENEW_TIME "365d" #define DEFAULT_SKEW (krb5_deltat) 600 static char *keytab_filename = nullptr, *principal_name = nullptr; static krb5_keytab keytab = nullptr; static krb5_keytab_entry entry; static krb5_kt_cursor cursor; static krb5_creds *creds = nullptr; #if USE_HEIMDAL_KRB5 && !HAVE_KRB5_GET_RENEWED_CREDS static krb5_creds creds2; #endif static krb5_principal principal = nullptr; static krb5_deltat skew; #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC krb5_get_init_creds_opt *options; #else krb5_get_init_creds_opt options; #endif krb5_error_code code = 0; krb5_deltat rlife; #if HAVE_PROFILE_H && HAVE_KRB5_GET_PROFILE && HAVE_PROFILE_GET_INTEGER && HAVE_PROFILE_RELEASE profile_t profile; #endif #if USE_HEIMDAL_KRB5 && !HAVE_KRB5_GET_RENEWED_CREDS krb5_kdc_flags flags; #if HAVE_KRB5_PRINCIPAL_GET_REALM const char *client_realm; #else krb5_realm client_realm; #endif #endif char *mem_cache; restart: /* * Check if credentials need to be renewed */ if (creds && (creds->times.endtime - time(nullptr) > skew) && (creds->times.renew_till - time(nullptr) > 2 * skew)) { if (creds->times.endtime - time(nullptr) < 2 * skew) { #if HAVE_KRB5_GET_RENEWED_CREDS /* renew ticket */ code = krb5_get_renewed_creds(kparam.context, creds, principal, kparam.cc, nullptr); #else /* renew ticket */ flags.i = 0; flags.b.renewable = flags.b.renew = 1; code = krb5_cc_get_principal(kparam.context, kparam.cc, &creds2.client); if (code) { debugs(11, 5, "Error while getting principal from credential cache : " << error_message(code)); return (1); } #if HAVE_KRB5_PRINCIPAL_GET_REALM client_realm = krb5_principal_get_realm(kparam.context, principal); #else client_realm = krb5_princ_realm(kparam.context, creds2.client); #endif code = krb5_make_principal(kparam.context, &creds2.server, (krb5_const_realm)&client_realm, KRB5_TGS_NAME, (krb5_const_realm)&client_realm, nullptr); if (code) { debugs(11, 5, "Error while getting krbtgt principal : " << error_message(code)); return (1); } code = krb5_get_kdc_cred(kparam.context, kparam.cc, flags, nullptr, nullptr, &creds2, &creds); krb5_free_creds(kparam.context, &creds2); #endif if (code) { if (code == KRB5KRB_AP_ERR_TKT_EXPIRED) { krb5_free_creds(kparam.context, creds); creds = nullptr; /* this can happen because of clock skew */ goto restart; } debugs(11, 5, "Error while get credentials : " << error_message(code)); return (1); } } } else { /* reinit */ if (!kparam.context) { code = krb5_init_context(&kparam.context); if (code) { debugs(11, 5, "Error while initialising Kerberos library : " << error_message(code)); return (1); } } #if HAVE_PROFILE_H && HAVE_KRB5_GET_PROFILE && HAVE_PROFILE_GET_INTEGER && HAVE_PROFILE_RELEASE code = krb5_get_profile(kparam.context, &profile); if (code) { if (profile) profile_release(profile); debugs(11, 5, "Error while getting profile : " << error_message(code)); return (1); } code = profile_get_integer(profile, "libdefaults", "clockskew", nullptr, 5 * 60, &skew); if (profile) profile_release(profile); if (code) { debugs(11, 5, "Error while getting clockskew : " << error_message(code)); return (1); } #elif USE_HEIMDAL_KRB5 && HAVE_KRB5_GET_MAX_TIME_SKEW skew = krb5_get_max_time_skew(kparam.context); #elif USE_HEIMDAL_KRB5 && HAVE_MAX_SKEW_IN_KRB5_CONTEXT skew = kparam.context->max_skew; #else skew = DEFAULT_SKEW; #endif if (!kf) { char buf[KT_PATH_MAX], *p; krb5_kt_default_name(kparam.context, buf, KT_PATH_MAX); p = strchr(buf, ':'); if (p) ++p; xfree(keytab_filename); keytab_filename = xstrdup(p ? p : buf); } else { keytab_filename = xstrdup(kf); } code = krb5_kt_resolve(kparam.context, keytab_filename, &keytab); if (code) { debugs(11, 5, "Error while resolving keytab filename " << keytab_filename << " : " << error_message(code)); return (1); } if (!pn) { code = krb5_kt_start_seq_get(kparam.context, keytab, &cursor); if (code) { debugs(11, 5, "Error while starting keytab scan : " << error_message(code)); return (1); } code = krb5_kt_next_entry(kparam.context, keytab, &entry, &cursor); krb5_copy_principal(kparam.context, entry.principal, &principal); if (code && code != KRB5_KT_END) { debugs(11, 5, "Error while scanning keytab : " << error_message(code)); return (1); } code = krb5_kt_end_seq_get(kparam.context, keytab, &cursor); if (code) { debugs(11, 5, "Error while ending keytab scan : " << error_message(code)); return (1); } #if USE_HEIMDAL_KRB5 || ( HAVE_KRB5_KT_FREE_ENTRY && HAVE_DECL_KRB5_KT_FREE_ENTRY) code = krb5_kt_free_entry(kparam.context, &entry); #else code = krb5_free_keytab_entry_contents(kparam.context, &entry); #endif if (code) { debugs(11, 5, "Error while freeing keytab entry : " << error_message(code)); return (1); } } else { principal_name = xstrdup(pn); } if (!principal) { code = krb5_parse_name(kparam.context, principal_name, &principal); if (code) { debugs(11, 5, "Error while parsing principal name " << principal_name << " : " << error_message(code)); return (1); } } creds = (krb5_creds *) xmalloc(sizeof(*creds)); memset(creds, 0, sizeof(*creds)); #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC krb5_get_init_creds_opt_alloc(kparam.context, &options); #else krb5_get_init_creds_opt_init(&options); #endif code = krb5_string_to_deltat((char *) MAX_RENEW_TIME, &rlife); if (code != 0 || rlife == 0) { debugs(11, 5, "Error bad lifetime value " << MAX_RENEW_TIME << " : " << error_message(code)); return (1); } #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC krb5_get_init_creds_opt_set_renew_life(options, rlife); code = krb5_get_init_creds_keytab(kparam.context, creds, principal, keytab, 0, nullptr, options); #if HAVE_KRB5_GET_INIT_CREDS_FREE_CONTEXT krb5_get_init_creds_opt_free(kparam.context, options); #else krb5_get_init_creds_opt_free(options); #endif #else krb5_get_init_creds_opt_set_renew_life(&options, rlife); code = krb5_get_init_creds_keytab(kparam.context, creds, principal, keytab, 0, nullptr, &options); #endif if (code) { debugs(11, 5, "Error while initializing credentials from keytab : " << error_message(code)); return (1); } #if !HAVE_KRB5_MEMORY_CACHE mem_cache = (char *) xmalloc(strlen("FILE:/tmp/peer_proxy_negotiate_auth_") + 16); if (!mem_cache) { debugs(11, 5, "Error while allocating memory"); return(1); } snprintf(mem_cache, strlen("FILE:/tmp/peer_proxy_negotiate_auth_") + 16, "FILE:/tmp/peer_proxy_negotiate_auth_%d", (int) getpid()); #else mem_cache = (char *) xmalloc(strlen("MEMORY:peer_proxy_negotiate_auth_") + 16); if (!mem_cache) { debugs(11, 5, "Error while allocating memory"); return(1); } snprintf(mem_cache, strlen("MEMORY:peer_proxy_negotiate_auth_") + 16, "MEMORY:peer_proxy_negotiate_auth_%d", (int) getpid()); #endif setenv("KRB5CCNAME", mem_cache, 1); code = krb5_cc_resolve(kparam.context, mem_cache, &kparam.cc); xfree(mem_cache); if (code) { debugs(11, 5, "Error while resolving memory credential cache : " << error_message(code)); return (1); } code = krb5_cc_initialize(kparam.context, kparam.cc, principal); if (code) { debugs(11, 5, "Error while initializing memory credential cache : " << error_message(code)); return (1); } code = krb5_cc_store_cred(kparam.context, kparam.cc, creds); if (code) { debugs(11, 5, "Error while storing credentials : " << error_message(code)); return (1); } if (!creds->times.starttime) creds->times.starttime = creds->times.authtime; } return (0); } /* * peer_proxy_negotiate_auth gets a GSSAPI token for principal_name * and base64 encodes it. */ char *peer_proxy_negotiate_auth(char *principal_name, char *proxy, int flags) { int rc = 0; OM_uint32 major_status, minor_status; gss_ctx_id_t gss_context = GSS_C_NO_CONTEXT; gss_name_t server_name = GSS_C_NO_NAME; gss_buffer_desc service = GSS_C_EMPTY_BUFFER; gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; char *token = nullptr; setbuf(stdout, nullptr); setbuf(stdin, nullptr); if (!proxy) { debugs(11, 5, "Error : No proxy server name"); return nullptr; } if (!(flags & PEER_PROXY_NEGOTIATE_NOKEYTAB)) { if (principal_name) debugs(11, 5, "Creating credential cache for " << principal_name); else debugs(11, 5, "Creating credential cache"); rc = krb5_create_cache(nullptr, principal_name); if (rc) { debugs(11, 5, "Error : Failed to create Kerberos cache"); krb5_cleanup(); return nullptr; } } service.value = (void *) xmalloc(strlen("HTTP") + strlen(proxy) + 2); snprintf((char *) service.value, strlen("HTTP") + strlen(proxy) + 2, "%s@%s", "HTTP", proxy); service.length = strlen((char *) service.value); debugs(11, 5, "Import gss name"); major_status = gss_import_name(&minor_status, &service, gss_nt_service_name, &server_name); if (check_gss_err(major_status, minor_status, "gss_import_name()")) goto cleanup; debugs(11, 5, "Initialize gss security context"); major_status = gss_init_sec_context(&minor_status, GSS_C_NO_CREDENTIAL, &gss_context, server_name, gss_mech_spnego, 0, 0, GSS_C_NO_CHANNEL_BINDINGS, &input_token, nullptr, &output_token, nullptr, nullptr); if (check_gss_err(major_status, minor_status, "gss_init_sec_context()")) goto cleanup; debugs(11, 5, "Got token with length " << output_token.length); if (output_token.length) { static char b64buf[8192]; // XXX: 8KB only because base64_encode_bin() used to. struct base64_encode_ctx ctx; base64_encode_init(&ctx); size_t blen = base64_encode_update(&ctx, b64buf, output_token.length, reinterpret_cast(output_token.value)); blen += base64_encode_final(&ctx, b64buf+blen); b64buf[blen] = '\0'; token = reinterpret_cast(b64buf); } cleanup: gss_delete_sec_context(&minor_status, &gss_context, nullptr); gss_release_buffer(&minor_status, &service); gss_release_buffer(&minor_status, &input_token); gss_release_buffer(&minor_status, &output_token); gss_release_name(&minor_status, &server_name); return token; } #endif /* HAVE_AUTH_MODULE_NEGOTIATE && HAVE_KRB5 && HAVE_GSSAPI */