2 * Copyright (C) 1996-2025 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
10 * DEBUG: 11 Hypertext Transfer Protocol (HTTP)
15 #if HAVE_AUTH_MODULE_NEGOTIATE && HAVE_KRB5 && HAVE_GSSAPI
17 #include "compat/krb5.h"
18 #include "debug/Stream.h"
19 #include "peer_proxy_negotiate_auth.h"
23 #endif /* HAVE_PROFILE_H */
24 #if HAVE_ET_COM_ERR_H && !HAVE_KRB5_H
25 #include <et/com_err.h>
26 #endif /* HAVE_COM_ERR_H */
29 #endif /* HAVE_COM_ERR_H */
34 #define GSSKRB_APPLE_DEPRECATED(x)
36 #if HAVE_GSSAPI_GSSAPI_H
37 #include <gssapi/gssapi.h>
40 #endif /* HAVE_GSSAPI_H */
41 #if HAVE_GSSAPI_GSSAPI_EXT_H
42 #include <gssapi/gssapi_ext.h>
43 #endif /* HAVE_GSSAPI_GSSAPI_EXT_H */
44 #if HAVE_GSSAPI_GSSAPI_KRB5_H
45 #include <gssapi/gssapi_krb5.h>
46 #endif /* HAVE_GSSAPI_GSSAPI_KRB5_H */
47 #if HAVE_GSSAPI_GSSAPI_GENERIC_H
48 #include <gssapi/gssapi_generic.h>
49 #endif /* HAVE_GSSAPI_GSSAPI_GENERIC_H */
51 #ifndef gss_nt_service_name
52 #define gss_nt_service_name GSS_C_NT_HOSTBASED_SERVICE
55 #if !HAVE_ERROR_MESSAGE && HAVE_KRB5_GET_ERROR_MESSAGE
56 #define error_message(code) krb5_get_error_message(kparam.context,code)
57 #elif !HAVE_ERROR_MESSAGE && HAVE_KRB5_GET_ERR_TEXT
58 #define error_message(code) krb5_get_err_text(kparam.context,code)
59 #elif !HAVE_ERROR_MESSAGE
60 static char err_code
[17];
61 const char *KRB5_CALLCONV
62 error_message(long code
) {
63 snprintf(err_code
,16,"%ld",code
);
68 #ifndef gss_mech_spnego
69 static gss_OID_desc _gss_mech_spnego
=
70 { 6, (void *) "\x2b\x06\x01\x05\x05\x02" };
71 gss_OID gss_mech_spnego
= &_gss_mech_spnego
;
75 #include <ibm_svc/krb5_svc.h>
76 const char *KRB5_CALLCONV
error_message(long code
) {
78 krb5_svc_get_msg(code
, &msg
);
84 * Kerberos context and cache structure
85 * Caches authentication details to reduce
86 * number of authentication requests to kdc
88 static struct kstruct
{
96 * krb5_create_cache creates a Kerberos file credential cache or a memory
97 * credential cache if supported. The initial key for the principal
98 * principal_name is extracted from the keytab keytab_filename.
100 * If keytab_filename is NULL the default will be used.
101 * If principal_name is NULL the first working entry of the keytab will be used.
103 int krb5_create_cache(char *keytab_filename
, char *principal_name
);
106 * krb5_cleanup clears used Keberos memory
108 void krb5_cleanup(void);
111 * check_gss_err checks for gssapi error codes, extracts the error message
114 int check_gss_err(OM_uint32 major_status
, OM_uint32 minor_status
,
115 const char *function
);
117 int check_gss_err(OM_uint32 major_status
, OM_uint32 minor_status
,
118 const char *function
) {
119 if (GSS_ERROR(major_status
)) {
120 OM_uint32 maj_stat
, min_stat
;
121 OM_uint32 msg_ctx
= 0;
122 gss_buffer_desc status_string
;
129 /* convert major status code (GSS-API error) to text */
130 maj_stat
= gss_display_status(&min_stat
, major_status
,
131 GSS_C_GSS_CODE
, GSS_C_NULL_OID
, &msg_ctx
, &status_string
);
132 if (maj_stat
== GSS_S_COMPLETE
) {
133 if (sizeof(buf
) > len
+ status_string
.length
+ 1) {
134 memcpy(buf
+ len
, status_string
.value
,
135 status_string
.length
);
136 len
+= status_string
.length
;
138 gss_release_buffer(&min_stat
, &status_string
);
141 gss_release_buffer(&min_stat
, &status_string
);
143 if (sizeof(buf
) > len
+ 2) {
144 strcpy(buf
+ len
, ". ");
149 /* convert minor status code (underlying routine error) to text */
150 maj_stat
= gss_display_status(&min_stat
, minor_status
,
151 GSS_C_MECH_CODE
, GSS_C_NULL_OID
, &msg_ctx
, &status_string
);
152 if (maj_stat
== GSS_S_COMPLETE
) {
153 if (sizeof(buf
) > len
+ status_string
.length
) {
154 memcpy(buf
+ len
, status_string
.value
,
155 status_string
.length
);
156 len
+= status_string
.length
;
158 gss_release_buffer(&min_stat
, &status_string
);
161 gss_release_buffer(&min_stat
, &status_string
);
163 debugs(11, 5, function
<< "failed: " << buf
);
169 void krb5_cleanup() {
170 debugs(11, 5, "Cleanup kerberos context");
171 if (kparam
.context
) {
173 krb5_cc_destroy(kparam
.context
, kparam
.cc
);
175 krb5_free_context(kparam
.context
);
176 kparam
.context
= nullptr;
180 int krb5_create_cache(char *kf
, char *pn
) {
182 #define KT_PATH_MAX 256
183 #define MAX_RENEW_TIME "365d"
184 #define DEFAULT_SKEW (krb5_deltat) 600
186 static char *keytab_filename
= nullptr, *principal_name
= nullptr;
187 static krb5_keytab keytab
= nullptr;
188 static krb5_keytab_entry entry
;
189 static krb5_kt_cursor cursor
;
190 static krb5_creds
*creds
= nullptr;
191 #if HAVE_LIBHEIMDAL_KRB5 && !HAVE_KRB5_GET_RENEWED_CREDS
192 static krb5_creds creds2
;
194 static krb5_principal principal
= nullptr;
195 static krb5_deltat skew
;
197 #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
198 krb5_get_init_creds_opt
*options
;
200 krb5_get_init_creds_opt options
;
202 krb5_error_code code
= 0;
204 #if HAVE_PROFILE_H && HAVE_KRB5_GET_PROFILE && HAVE_PROFILE_GET_INTEGER && HAVE_PROFILE_RELEASE
207 #if HAVE_LIBHEIMDAL_KRB5 && !HAVE_KRB5_GET_RENEWED_CREDS
208 krb5_kdc_flags flags
;
209 #if HAVE_KRB5_PRINCIPAL_GET_REALM
210 const char *client_realm
;
212 krb5_realm client_realm
;
219 * Check if credentials need to be renewed
222 (creds
->times
.endtime
- time(nullptr) > skew
) &&
223 (creds
->times
.renew_till
- time(nullptr) > 2 * skew
)) {
224 if (creds
->times
.endtime
- time(nullptr) < 2 * skew
) {
225 #if HAVE_KRB5_GET_RENEWED_CREDS
228 krb5_get_renewed_creds(kparam
.context
, creds
, principal
,
233 flags
.b
.renewable
= flags
.b
.renew
= 1;
236 krb5_cc_get_principal(kparam
.context
, kparam
.cc
,
241 "Error while getting principal from credential cache : "
242 << error_message(code
));
245 #if HAVE_KRB5_PRINCIPAL_GET_REALM
246 client_realm
= krb5_principal_get_realm(kparam
.context
, principal
);
248 client_realm
= krb5_princ_realm(kparam
.context
, creds2
.client
);
251 krb5_make_principal(kparam
.context
, &creds2
.server
,
252 (krb5_const_realm
)&client_realm
, KRB5_TGS_NAME
,
253 (krb5_const_realm
)&client_realm
, nullptr);
256 "Error while getting krbtgt principal : " <<
257 error_message(code
));
261 krb5_get_kdc_cred(kparam
.context
, kparam
.cc
, flags
, nullptr,
262 nullptr, &creds2
, &creds
);
263 krb5_free_creds(kparam
.context
, &creds2
);
266 if (code
== KRB5KRB_AP_ERR_TKT_EXPIRED
) {
267 krb5_free_creds(kparam
.context
, creds
);
269 /* this can happen because of clock skew */
273 "Error while get credentials : " <<
274 error_message(code
));
280 if (!kparam
.context
) {
281 code
= krb5_init_context(&kparam
.context
);
284 "Error while initialising Kerberos library : "
285 << error_message(code
));
289 #if HAVE_PROFILE_H && HAVE_KRB5_GET_PROFILE && HAVE_PROFILE_GET_INTEGER && HAVE_PROFILE_RELEASE
290 code
= krb5_get_profile(kparam
.context
, &profile
);
293 profile_release(profile
);
295 "Error while getting profile : " <<
296 error_message(code
));
300 profile_get_integer(profile
, "libdefaults", "clockskew", nullptr,
303 profile_release(profile
);
306 "Error while getting clockskew : " <<
307 error_message(code
));
310 #elif HAVE_LIBHEIMDAL_KRB5
311 skew
= krb5_get_max_time_skew(kparam
.context
);
317 char buf
[KT_PATH_MAX
], *p
;
319 krb5_kt_default_name(kparam
.context
, buf
, KT_PATH_MAX
);
320 p
= strchr(buf
, ':');
323 xfree(keytab_filename
);
324 keytab_filename
= xstrdup(p
? p
: buf
);
326 keytab_filename
= xstrdup(kf
);
329 code
= krb5_kt_resolve(kparam
.context
, keytab_filename
, &keytab
);
332 "Error while resolving keytab filename " <<
333 keytab_filename
<< " : " << error_message(code
));
338 code
= krb5_kt_start_seq_get(kparam
.context
, keytab
, &cursor
);
341 "Error while starting keytab scan : " <<
342 error_message(code
));
346 krb5_kt_next_entry(kparam
.context
, keytab
, &entry
, &cursor
);
347 krb5_copy_principal(kparam
.context
, entry
.principal
,
349 if (code
&& code
!= KRB5_KT_END
) {
351 "Error while scanning keytab : " <<
352 error_message(code
));
356 code
= krb5_kt_end_seq_get(kparam
.context
, keytab
, &cursor
);
359 "Error while ending keytab scan : " <<
360 error_message(code
));
363 #if HAVE_LIBHEIMDAL_KRB5 || ( HAVE_KRB5_KT_FREE_ENTRY && HAVE_DECL_KRB5_KT_FREE_ENTRY)
364 code
= krb5_kt_free_entry(kparam
.context
, &entry
);
366 code
= krb5_free_keytab_entry_contents(kparam
.context
, &entry
);
370 "Error while freeing keytab entry : " <<
371 error_message(code
));
376 principal_name
= xstrdup(pn
);
381 krb5_parse_name(kparam
.context
, principal_name
, &principal
);
384 "Error while parsing principal name " <<
385 principal_name
<< " : " << error_message(code
));
390 creds
= (krb5_creds
*) xmalloc(sizeof(*creds
));
391 memset(creds
, 0, sizeof(*creds
));
392 #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
393 krb5_get_init_creds_opt_alloc(kparam
.context
, &options
);
395 krb5_get_init_creds_opt_init(&options
);
397 code
= krb5_string_to_deltat((char *) MAX_RENEW_TIME
, &rlife
);
398 if (code
!= 0 || rlife
== 0) {
400 "Error bad lifetime value " << MAX_RENEW_TIME
<<
401 " : " << error_message(code
));
404 #if HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
405 krb5_get_init_creds_opt_set_renew_life(options
, rlife
);
407 krb5_get_init_creds_keytab(kparam
.context
, creds
, principal
,
408 keytab
, 0, nullptr, options
);
409 #if HAVE_KRB5_GET_INIT_CREDS_FREE_CONTEXT
410 krb5_get_init_creds_opt_free(kparam
.context
, options
);
412 krb5_get_init_creds_opt_free(options
);
415 krb5_get_init_creds_opt_set_renew_life(&options
, rlife
);
417 krb5_get_init_creds_keytab(kparam
.context
, creds
, principal
,
418 keytab
, 0, nullptr, &options
);
423 "Error while initializing credentials from keytab : " <<
424 error_message(code
));
427 #if !HAVE_KRB5_MEMORY_CACHE
429 (char *) xmalloc(strlen("FILE:/tmp/peer_proxy_negotiate_auth_")
432 debugs(11, 5, "Error while allocating memory");
436 strlen("FILE:/tmp/peer_proxy_negotiate_auth_") + 16,
437 "FILE:/tmp/peer_proxy_negotiate_auth_%d", (int) getpid());
440 (char *) xmalloc(strlen("MEMORY:peer_proxy_negotiate_auth_") +
443 debugs(11, 5, "Error while allocating memory");
447 strlen("MEMORY:peer_proxy_negotiate_auth_") + 16,
448 "MEMORY:peer_proxy_negotiate_auth_%d", (int) getpid());
451 setenv("KRB5CCNAME", mem_cache
, 1);
452 code
= krb5_cc_resolve(kparam
.context
, mem_cache
, &kparam
.cc
);
456 "Error while resolving memory credential cache : "
457 << error_message(code
));
460 code
= krb5_cc_initialize(kparam
.context
, kparam
.cc
, principal
);
464 "Error while initializing memory credential cache : " <<
465 error_message(code
));
468 code
= krb5_cc_store_cred(kparam
.context
, kparam
.cc
, creds
);
471 "Error while storing credentials : " <<
472 error_message(code
));
476 if (!creds
->times
.starttime
)
477 creds
->times
.starttime
= creds
->times
.authtime
;
483 * peer_proxy_negotiate_auth gets a GSSAPI token for principal_name
484 * and base64 encodes it.
486 char *peer_proxy_negotiate_auth(char *principal_name
, const char * const proxy
, int flags
) {
488 OM_uint32 major_status
, minor_status
;
489 gss_ctx_id_t gss_context
= GSS_C_NO_CONTEXT
;
490 gss_name_t server_name
= GSS_C_NO_NAME
;
491 gss_buffer_desc service
= GSS_C_EMPTY_BUFFER
;
492 gss_buffer_desc input_token
= GSS_C_EMPTY_BUFFER
;
493 gss_buffer_desc output_token
= GSS_C_EMPTY_BUFFER
;
494 char *token
= nullptr;
496 setbuf(stdout
, nullptr);
497 setbuf(stdin
, nullptr);
500 debugs(11, 5, "Error : No proxy server name");
504 if (!(flags
& PEER_PROXY_NEGOTIATE_NOKEYTAB
)) {
507 "Creating credential cache for " << principal_name
);
509 debugs(11, 5, "Creating credential cache");
510 rc
= krb5_create_cache(nullptr, principal_name
);
512 debugs(11, 5, "Error : Failed to create Kerberos cache");
518 service
.value
= (void *) xmalloc(strlen("HTTP") + strlen(proxy
) + 2);
519 snprintf((char *) service
.value
, strlen("HTTP") + strlen(proxy
) + 2,
520 "%s@%s", "HTTP", proxy
);
521 service
.length
= strlen((char *) service
.value
);
523 debugs(11, 5, "Import gss name");
524 major_status
= gss_import_name(&minor_status
, &service
,
525 gss_nt_service_name
, &server_name
);
527 if (check_gss_err(major_status
, minor_status
, "gss_import_name()"))
530 debugs(11, 5, "Initialize gss security context");
531 major_status
= gss_init_sec_context(&minor_status
,
538 GSS_C_NO_CHANNEL_BINDINGS
,
539 &input_token
, nullptr, &output_token
, nullptr, nullptr);
541 if (check_gss_err(major_status
, minor_status
, "gss_init_sec_context()"))
544 debugs(11, 5, "Got token with length " << output_token
.length
);
545 if (output_token
.length
) {
546 static char b64buf
[8192]; // XXX: 8KB only because base64_encode_bin() used to.
547 struct base64_encode_ctx ctx
;
548 base64_encode_init(&ctx
);
549 size_t blen
= base64_encode_update(&ctx
, b64buf
, output_token
.length
, reinterpret_cast<const uint8_t*>(output_token
.value
));
550 blen
+= base64_encode_final(&ctx
, b64buf
+blen
);
553 token
= reinterpret_cast<char*>(b64buf
);
557 gss_delete_sec_context(&minor_status
, &gss_context
, nullptr);
558 gss_release_buffer(&minor_status
, &service
);
559 gss_release_buffer(&minor_status
, &input_token
);
560 gss_release_buffer(&minor_status
, &output_token
);
561 gss_release_name(&minor_status
, &server_name
);
566 #endif /* HAVE_AUTH_MODULE_NEGOTIATE && HAVE_KRB5 && HAVE_GSSAPI */